Skip to content

fix(cache): reclaim expired entries in set() - #623

Merged
alexandretrotel merged 7 commits into
mainfrom
622-cache-set-leaves-expired-entries-in-store-and-policy-breaking-capacity-bounds-and-retaining-dead-entries
Nov 15, 2026
Merged

alexandretrotel merged 7 commits into
mainfrom
622-cache-set-leaves-expired-entries-in-store-and-policy-breaking-capacity-bounds-and-retaining-dead-entries

Conversation

@alexandretrotel

@alexandretrotel alexandretrotel commented Nov 15, 2026 •

Copy link
Copy Markdown
Member

Closes #622

Problem

set() decided whether to evict by counting only live entries, but it never removed the expired ones. As a result:

  • with capacity: 2, after two entries expired unread, the cache could end up holding 4 live entries;
  • with short TTLs, eviction never ran, so every expired value stayed in memory (10,000 entries in a cache of 10), and onEvict never fired.

Fix

When a new key is inserted into a full cache, set() first drops all expired entries (firing onEvict), and evicts a live entry only if the cache is still full. The store now never holds more than capacity entries.

Changes

  • packages/cache/src/cache.ts: dropAllExpired() + evictOne() helpers used by set()
  • Regression tests for both scenarios from the issue
  • docs/cache/ttl.md and docs/cache/on-evict.md updated
  • Patch bump: @zap-studio/cache 1.0.0 → 1.0.1

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Nov 15, 2026 •

Copy link
Copy Markdown

Todo Tree Summary

No TODOs found!

Last updated Nov 15, 2026, 10:29 UTC · todo-tree

@github-actions

github-actions Bot commented Nov 15, 2026 •

Copy link
Copy Markdown

bumpy-frog

The changes in this PR will be included in the next version bump.

patch Patch releases

  • @zap-studio/cache 1.0.0 → 1.0.1

Bump files in this PR

Click here if you want to add another bump file to this PR


This comment is maintained by bumpy.

@codecov

codecov Bot commented Nov 15, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ All tests successful. No failed tests found.

📢 Thoughts on this report? Let us know!

@alexandretrotel alexandretrotel changed the title 622 cache set leaves expired entries in store and policy breaking capacity bounds and retaining dead entries fix(cache): reclaim expired entries in set() Nov 15, 2026
@greptile-apps

greptile-apps Bot commented Nov 15, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 4/5

[Medium impact] The change affects cache behavior for consumers of the cache package.

Fix the sweep deleting refreshed values before merging.

Fix All in Claude CodeFindings

  1. P1 Refreshed values get deleted ▶
Fix with agent prompt
### Issue 1
packages/cache/src/cache.ts:88-90
`dropAllExpired()` can delete a fresh value written by `onEvict`. With two expired entries, `a` and `b`, suppose the callback for `a` refreshes `b` with a longer TTL. The second loop still calls `dropExpired()` with the old entry for `b`, deleting the replacement and reporting the old value to `onEvict`.

Before dropping each saved entry, check that `store.get(key)` still equals that entry.

```suggestion
    for (const [key, entry] of expired) {
      if (store.get(key) === entry) {
        dropExpired(key, entry);
      }
    }
```

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

The PR reclaims expired entries when set() inserts into a full cache.

  • Full caches clear expired entries before making room for a new key.

Reviews (2) · Last reviewed commit: "docs(cache): explain nuance in docs" · Reviewed by Greptile

Comment thread packages/cache/src/cache.ts Resolved
Comment thread docs/cache/ttl.md Resolved
@alexandretrotel

Copy link
Copy Markdown
Member Author

@greptileai review

Comment on lines +88 to +90
for (const [key, entry] of expired) {
dropExpired(key, entry);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Refreshed values get deleted

dropAllExpired() can delete a fresh value written by onEvict. With two expired entries, a and b, suppose the callback for a refreshes b with a longer TTL. The second loop still calls dropExpired() with the old entry for b, deleting the replacement and reporting the old value to onEvict.

Before dropping each saved entry, check that store.get(key) still equals that entry.

Suggested change
for (const [key, entry] of expired) {
dropExpired(key, entry);
}
for (const [key, entry] of expired) {
if (store.get(key) === entry) {
dropExpired(key, entry);
}
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/cache/src/cache.ts
Line: 88-90

Comment:
**Refreshed values get deleted**

`dropAllExpired()` can delete a fresh value written by `onEvict`. With two expired entries, `a` and `b`, suppose the callback for `a` refreshes `b` with a longer TTL. The second loop still calls `dropExpired()` with the old entry for `b`, deleting the replacement and reporting the old value to `onEvict`.

Before dropping each saved entry, check that `store.get(key)` still equals that entry.

```suggestion
    for (const [key, entry] of expired) {
      if (store.get(key) === entry) {
        dropExpired(key, entry);
      }
    }
```

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude Code Fix in Codex

@alexandretrotel
alexandretrotel merged commit b447945 into main Nov 15, 2026
10 checks passed
@alexandretrotel
alexandretrotel deleted the 622-cache-set-leaves-expired-entries-in-store-and-policy-breaking-capacity-bounds-and-retaining-dead-entries branch November 15, 2026 10:31
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.

cache: set() leaves expired entries in store and policy, breaking capacity bounds and retaining dead entries

1 participant