| diff --git a/node_modules/zod/src/v4/core/memoizer.ts b/node_modules/zod/src/v4/core/memoizer.ts | |
| index c2145c0..6fc6cde 100644 | |
| --- a/node_modules/zod/src/v4/core/memoizer.ts | |
| +++ b/node_modules/zod/src/v4/core/memoizer.ts | |
| @@ -240,9 +240,6 @@ const memo: $ZodMemoizer = { | |
| attach(inst) { | |
| let isRecursiveInst: boolean | undefined; | |
| let rechecked = false; | |
| - // a recursive schema is re-entered many times per parse and its bucket never changes | |
| - let lastCtx: object | undefined; | |
| - let lastBucket: WeakMap<object, Entry> | undefined; | |
| // 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. | |
| inst._zod.deferred ??= []; | |
| @@ -272,14 +269,7 @@ const memo: $ZodMemoizer = { | |
| (ctx as WithState)[STATE] = state; | |
| } | |
| - let bucket: WeakMap<object, Entry>; | |
| - if (lastCtx === ctx) { | |
| - bucket = lastBucket!; | |
| - } else { | |
| - bucket = bucketFor(state, inst); | |
| - lastCtx = ctx; | |
| - lastBucket = bucket; | |
| - } | |
| + const bucket = bucketFor(state, inst); | |
| const hit = bucket.get(input); | |
| if (hit) { | |
| @@ -295,12 +285,18 @@ const memo: $ZodMemoizer = { | |
| return payload; | |
| } | |
| + const previousHandoff = handoff; | |
| 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; | |
| + let result: util.MaybeAsync<ParsePayload>; | |
| + let entry: Entry | undefined; | |
| + try { | |
| + result = base(payload, ctx); | |
| + } finally { | |
| + handoff = previousHandoff; | |
| + // A container that rejected its input outright allocated nothing. | |
| + 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) { | |
| diff --git a/node_modules/zod/v4/core/memoizer.cjs b/node_modules/zod/v4/core/memoizer.cjs | |
| index cab19c2..732d2f4 100644 | |
| --- a/node_modules/zod/v4/core/memoizer.cjs | |
| +++ b/node_modules/zod/v4/core/memoizer.cjs | |
| @@ -251,9 +251,6 @@ const memo = { | |
| var _a; | |
| let isRecursiveInst; | |
| let rechecked = false; | |
| - // a recursive schema is re-entered many times per parse 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(() => { | |
| @@ -282,15 +279,7 @@ const memo = { | |
| state = { buckets: new WeakMap(), backEdges: undefined }; | |
| ctx[STATE] = state; | |
| } | |
| - let bucket; | |
| - if (lastCtx === ctx) { | |
| - bucket = lastBucket; | |
| - } | |
| - else { | |
| - bucket = bucketFor(state, inst); | |
| - lastCtx = ctx; | |
| - lastBucket = bucket; | |
| - } | |
| + const bucket = bucketFor(state, inst); | |
| const hit = bucket.get(input); | |
| if (hit) { | |
| payload.value = hit.value; | |
| @@ -306,12 +295,19 @@ const memo = { | |
| } | |
| return payload; | |
| } | |
| + const previousHandoff = handoff; | |
| 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; | |
| + let result; | |
| + let entry; | |
| + try { | |
| + result = base(payload, ctx); | |
| + } | |
| + finally { | |
| + handoff = previousHandoff; | |
| + // A container that rejected its input outright allocated nothing. | |
| + 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) => { | |
| diff --git a/node_modules/zod/v4/core/memoizer.js b/node_modules/zod/v4/core/memoizer.js | |
| index 393fb06..222d470 100644 | |
| --- a/node_modules/zod/v4/core/memoizer.js | |
| +++ b/node_modules/zod/v4/core/memoizer.js | |
| @@ -221,9 +221,6 @@ const memo = { | |
| var _a; | |
| let isRecursiveInst; | |
| let rechecked = false; | |
| - // a recursive schema is re-entered many times per parse 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(() => { | |
| @@ -252,15 +249,7 @@ const memo = { | |
| state = { buckets: new WeakMap(), backEdges: undefined }; | |
| ctx[STATE] = state; | |
| } | |
| - let bucket; | |
| - if (lastCtx === ctx) { | |
| - bucket = lastBucket; | |
| - } | |
| - else { | |
| - bucket = bucketFor(state, inst); | |
| - lastCtx = ctx; | |
| - lastBucket = bucket; | |
| - } | |
| + const bucket = bucketFor(state, inst); | |
| const hit = bucket.get(input); | |
| if (hit) { | |
| payload.value = hit.value; | |
| @@ -276,12 +265,19 @@ const memo = { | |
| } | |
| return payload; | |
| } | |
| + const previousHandoff = handoff; | |
| 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; | |
| + let result; | |
| + let entry; | |
| + try { | |
| + result = base(payload, ctx); | |
| + } | |
| + finally { | |
| + handoff = previousHandoff; | |
| + // A container that rejected its input outright allocated nothing. | |
| + 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) => { |