Skip to content

fix(cache): clone cached issues to stop issue paths from accumulating - #1620

Merged
fabian-hiller merged 2 commits into
open-circle:mainfrom
MaxFreedomPollard:fix-cache-issue-path-mutation
Sep 8, 2026
Merged

fabian-hiller merged 2 commits into
open-circle:mainfrom
MaxFreedomPollard:fix-cache-issue-path-mutation

Conversation

@MaxFreedomPollard

@MaxFreedomPollard MaxFreedomPollard commented Sep 6, 2026 •

Copy link
Copy Markdown
Contributor

A schema wrapped in cache that sits inside a parent schema grows an extra path item on its issues on every cache hit, so the reported path is wrong from the second parse onwards.

const schema = v.object({ key: v.cache(v.pipe(v.string(), v.minLength(3))) });

for (let index = 0; index < 3; index++) {
  const result = v.safeParse(schema, { key: 'fo' });
  console.log(v.getDotPath(result.issues![0]));
}
// key
// key.key
// key.key.key

v.flatten is affected the same way, which is how this shows up in form code: { nested: { key: [...] } } on the first parse, then { nested: { 'key.key': [...] } }, then { nested: { 'key.key.key': [...] } }.

Cause

_cloneDataset in library/src/utils/_cloneDataset/_cloneDataset.ts clones the issues array of the cached dataset but keeps the issue objects inside it:

issues: dataset.issues && [...dataset.issues],

Issues are mutable, and every parent schema mutates the ones it receives. For example object in library/src/schemas/object/object.ts does issue.path.unshift(pathItem) when a path already exists and issue.path = [pathItem] when it does not, and forward in library/src/methods/forward/forward.ts pushes onto the same array. Because cache and cacheAsync return the very same issue object on every hit, each parse appends its path item to the issue that is still sitting in the cache, and the path keeps growing. Cloning the array only prevents the parent from pushing further issues into the cached array, not from mutating the issues themselves.

The path array needs the same treatment as the issue object. When the cached schema already produces a path of its own, unshift mutates the array that the cached issue still points at.

Fix

_cloneDataset now clones each issue and its path array, so a parent schema and forward mutate the returned copy instead of the cached one. value is still copied by reference, and the sub issues of an issue are untouched because no schema mutates those.

Tests

library/src/utils/_cloneDataset/_cloneDataset.test.ts now asserts that the issues and their path arrays are copies rather than the originals, with the path items themselves still shared by reference. library/src/methods/cache/cache.test.ts and library/src/methods/cache/cacheAsync.test.ts each gain a regression test that parses the schema above three times and expects the dot path to stay key. All five assertions fail on main and pass with this change.

Verification

pnpm test in library: 530 files, 4609 tests passed, no type errors. pnpm lint in library (eslint, tsc --noEmit, deno check ./src/index.ts) and pnpm format.check both pass.

Summary by CodeRabbit

  • Bug Fixes

    • Fixed repeated validation runs with cached schemas incorrectly duplicating parent paths in reported issues.
    • Cached validation results now preserve consistent issue paths across repeated synchronous and asynchronous checks.
    • Improved isolation of cached issue data to prevent changes from carrying over between validation runs.
    • Ensured cached validation issues remain stable and accurate across multiple repeated checks.
  • Documentation

    • Added a changelog entry describing the corrected cached-validation behavior.

`_cloneDataset` in `library/src/utils/_cloneDataset/_cloneDataset.ts` copied
the `issues` array of a cached dataset but kept the issue objects themselves,
so `cache` and `cacheAsync` handed the same issue object back on every cache
hit. Parent schemas mutate the issues they receive, for example `object` in
`library/src/schemas/object/object.ts` runs `issue.path.unshift(pathItem)` or
`issue.path = [pathItem]`, so each hit added another path item to the cached
issue. Parsing `object({ key: cache(pipe(string(), minLength(3))) })` three
times produced the dot paths `key`, `key.key` and `key.key.key`, and the keys
of `flatten` grew with them.

`_cloneDataset` now clones each issue and its `path` array, so the mutations
of a parent schema or of `forward` apply to the returned copy instead of the
cached one.
@MaxFreedomPollard
MaxFreedomPollard force-pushed the fix-cache-issue-path-mutation branch from 7309a82 to 2a5b68b Compare September 6, 2026 02:45
@coderabbitai

coderabbitai Bot commented Sep 6, 2026 •

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 406a29c0-6465-4a99-9770-e5549b963309

📥 Commits

Reviewing files that changed from the base of the PR and between 2a5b68b and cb1ad4c.

📒 Files selected for processing (1)
  • library/src/utils/_cloneDataset/_cloneDataset.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • library/src/utils/_cloneDataset/_cloneDataset.ts

Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review.


Walkthrough

_cloneDataset now clones issue objects and their path arrays. Synchronous and asynchronous cache tests run repeated invalid nested-schema validations and verify that the parent path remains key without duplication. The changelog documents this behavior.

Priority: ⬇️ Low

Merge Risk: ⚪ Minimal · up to cb1ad

Cached validation issues and their path arrays are cloned to prevent paths accumulating across repeated parses. No current merge-blocking risk is identified.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: cloning cached issues to prevent issue paths from accumulating in cache and cacheAsync.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 4 files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@pkg-pr-new

pkg-pr-new Bot commented Sep 6, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/valibot@1620

commit: cb1ad4c

@yslpn
yslpn self-requested a review September 7, 2026 07:51
yslpn
yslpn previously approved these changes Sep 7, 2026

@yslpn yslpn left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, good fix. I have no issues with it. LGTM.

@yslpn yslpn self-assigned this Sep 7, 2026
@fabian-hiller

Copy link
Copy Markdown
Member

Thank you!

@fabian-hiller
fabian-hiller merged commit d65438c into open-circle:main Sep 8, 2026
17 checks passed
dadezzz pushed a commit to dadezzz/events-cash-register that referenced this pull request Sep 13, 2026
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [valibot](https://valibot.dev) ([source](https://github.com/open-circle/valibot)) | [`1.4.2` → `1.5.0`](https://renovatebot.com/diffs/npm/valibot/1.4.2/1.5.0) | ![age](https://developer.mend.io/api/mc/badges/age/npm/valibot/1.5.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/valibot/1.4.2/1.5.0?slim=true) |

---

### Release Notes

<details>
<summary>open-circle/valibot (valibot)</summary>

### [`v1.5.0`](https://github.com/open-circle/valibot/releases/tag/v1.5.0)

[Compare Source](open-circle/valibot@v1.4.2...v1.5.0)

Many thanks to [@&#8203;tats-u](https://github.com/tats-u), [@&#8203;idleberg](https://github.com/idleberg), [@&#8203;yslpn](https://github.com/yslpn), [@&#8203;francisjohnjohnston-web](https://github.com/francisjohnjohnston-web), [@&#8203;MaxFreedomPollard](https://github.com/MaxFreedomPollard), [@&#8203;mahirhir](https://github.com/mahirhir), [@&#8203;ItzXynx](https://github.com/ItzXynx), [@&#8203;LeSingh1](https://github.com/LeSingh1), [@&#8203;maxtaran2010](https://github.com/maxtaran2010), [@&#8203;ysknsid25](https://github.com/ysknsid25), [@&#8203;cyyynthia](https://github.com/cyyynthia), [@&#8203;spokodev](https://github.com/spokodev) and [@&#8203;sanjibani](https://github.com/sanjibani) for contributing to this release.

- Add `codePoints`, `maxCodePoints`, `minCodePoints` and `notCodePoints` validation actions to validate the number of Unicode code points (pull request [#&#8203;888](open-circle/valibot#888))
- Add `ksuid` validation action to validate KSUIDs (pull request [#&#8203;1370](open-circle/valibot#1370))
- Change Standard Schema properties to use eager initialization for faster schema construction and replace internal `_getStandardProps` utility with `_standardSchema` (pull request [#&#8203;1534](open-circle/valibot#1534))
- Change `url` action to use `URL.canParse` when available to avoid constructing URL objects (pull request [#&#8203;1608](open-circle/valibot#1608))
- Fix `stringifyJson` action to preserve the dataset value when `JSON.stringify` returns `undefined` (pull request [#&#8203;1476](open-circle/valibot#1476))
- Fix `literal` schema and `value`, `values`, `notValue` and `notValues` actions to treat `NaN` as equal to itself (pull request [#&#8203;1573](open-circle/valibot#1573))
- Fix `intersect` schema to merge matching `NaN` values and invalid dates (pull request [#&#8203;1573](open-circle/valibot#1573))
- Fix `cache` and `cacheAsync` methods to clone the issues of a cached dataset, preventing parent schemas from adding their path item to the same issue on every cache hit (pull request [#&#8203;1620](open-circle/valibot#1620))
- Fix `strictObject`, `looseObject`, `objectWithRest` and their async variants to correctly handle unknown input keys that collide with `Object.prototype` members (pull request [#&#8203;1523](open-circle/valibot#1523))
- Fix `intersect` and `intersectAsync` schemas to ignore inherited properties when merging objects and preserve own properties without invoking inherited setters or changing the output prototype (pull request [#&#8203;1621](open-circle/valibot#1621))
- Fix `ulid` action to reject ULIDs that exceed the maximum 128-bit value (pull request [#&#8203;1498](open-circle/valibot#1498))
- Fix `email` action to reject non-ASCII characters accepted by Unicode case folding (pull request [#&#8203;1075](open-circle/valibot#1075))

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Mend Renovate CLI](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC44Mi4xIiwidXBkYXRlZEluVmVyIjoiNDQuODIuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants