Skip to content

fix(cache): bound shared transform cache memory - #835

Merged
privatenumber merged 12 commits into
privatenumber:masterfrom
steipete:fix/bounded-transform-cache
Aug 30, 2026
Merged

privatenumber merged 12 commits into
privatenumber:masterfrom
steipete:fix/bounded-transform-cache

Conversation

@steipete

@steipete steipete commented Aug 28, 2026 •

Copy link
Copy Markdown
Contributor

Problem

tsx stores transformed modules in a shared disk cache so later processes can reuse them. The first cache lookup currently reads every filename into an in-memory index, including entries unrelated to the requested module.

As the shared cache grows, a small TypeScript program can exhaust its heap before its entrypoint runs. A cache with 713,453 unrelated entries reproduced the failure at a 128 MiB heap limit, and expiration of 100,000 entries could also exhaust memory after execution. Fixes #834.

Changes

Cache reads now probe only the requested SHA-1 key in the current and previous seven timestamp buckets. A lookup therefore uses constant memory regardless of how many unrelated files share the cache. Valid hits remain cached in the process, corrupt current entries can fall back to an older live entry, and reads do not create or maintain cache directories.

Writes still create the cache and schedule best-effort maintenance. Expiration now streams directory entries and processes them in batches of 64, which bounds outstanding unlink promises without retaining a directory-wide index. The cache filename format and seven-bucket retention policy remain unchanged.

The regression guard asserts that lookup never calls readdirSync(). It fails against the original implementation with one directory enumeration and passes with the bounded lookup. The focused suite also covers live-window lookup, corrupt entries, expiration boundaries, malformed filenames, and concurrent cache writers.

Replace the directory-wide metadata index with up to eight newest-first
probes across the live retention buckets.

Keep lookups read-only and let writers own cache creation and automatic
maintenance. Stream expiry in batches of at most 64 scanned entries, with
bounded deletions and an in-flight guard.

Refs: privatenumber#834
Seed the same 150000 real files without changing the 32 MiB child cap or
application assertions.

Await batches of 64 asynchronous writes to keep the watchdog responsive
and bound open descriptors. Name the empty and populated controls
separately so pending or failing output identifies the scenario.

Refs: privatenumber#834
Calibrate the real-file regression to 75,000 entries with a stricter
16 MiB heap cap. The original owner OOMs in five of five runs while the
repair passes, and repeated empty controls also pass at 12 MiB.

Preserve the same real-loader probe, 64-write batches, owned cache-enabled
environment, and exit/stdout assertions. Remove temporary diagnostics and
restore direct fixture disposal without increasing any deadline.

Refs: privatenumber#834
@privatenumber
privatenumber merged commit 28e1f12 into privatenumber:master Aug 30, 2026
2 checks passed
@privatenumber

Copy link
Copy Markdown
Owner

This issue is now resolved in v4.23.13.

If you're able to, your sponsorship would be very much appreciated.

@steipete

Copy link
Copy Markdown
Contributor Author

sweet, thank you!

@steipete

Copy link
Copy Markdown
Contributor Author

I already sponsor you through the OpenClaw Foundation, and just increased level from bronze to silver. Thanks again!

@privatenumber

Copy link
Copy Markdown
Owner

Thanks @steipete ! 🙌❤️

tiualvin pushed a commit to tiualvin/beyond-every-art that referenced this pull request Sep 4, 2026
Bounds the shared transform cache memory (privatenumber/tsx#835).

Replaces #139, which Dependabot closed and reopened as this once #142
landed the group's other half — eslint-config-next 15.5.25, which had to
move with `next` rather than on its own.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Shared transform cache size can exhaust heap before entrypoint execution

2 participants