Skip to content

fix(lru): actually evict stale entries from TtlLruCache on lookup#2974

Open
Tatlatat wants to merge 1 commit into
esengine:v1from
Tatlatat:fix/ttl-lru-evict-stale
Open

fix(lru): actually evict stale entries from TtlLruCache on lookup#2974
Tatlatat wants to merge 1 commit into
esengine:v1from
Tatlatat:fix/ttl-lru-evict-stale

Conversation

@Tatlatat
Copy link
Copy Markdown
Contributor

@Tatlatat Tatlatat commented Jun 3, 2026

What

TtlLruCache is documented as "a stale hit is treated as a miss and
evicted
"
, but get() didn't evict:

get(key: K): V | undefined {
  const e = this.inner.get(key);            // already promotes the entry to MRU
  if (!e) return undefined;
  if (e.expiresAt <= Date.now()) return undefined;  // miss — but entry stays
  return e.v;
}

Two problems for an expired key:

  1. It is never removed — it lingers in the underlying map.
  2. The preceding this.inner.get(key) has already re-inserted it as
    most-recently-used, so the stale entry outlives fresher ones during
    eviction.

TtlLruCache backs the gitignore cache and the directory-listing cache (both
5s TTL), which are hit on hot file-operation paths — so stale entries
accumulate and skew eviction under load.

Fix

  • Add a public delete(key) to LruCache.
  • In TtlLruCache.get, call this.inner.delete(key) when the entry is expired
    before returning undefined, so it is evicted immediately as documented.

Test

Appended a TtlLruCache case (fake timers): after the TTL elapses, a lookup
returns undefined, the key is gone from the underlying store, and it no longer
survives subsequent evictions. Verified it fails on the old code and passes on
the fix.

Verification

npx vitest run tests/lru.test.ts (11 passed); biome check clean on the
changed files.

TtlLruCache is documented as "a stale hit is treated as a miss and
evicted", but get() returned undefined for an expired entry without
removing it. Worse, the preceding this.inner.get(key) had already
re-inserted (promoted) the entry to most-recently-used, so a stale value
lingered in memory and wrongly outlived fresher entries during eviction —
a slow leak in the gitignore and directory-listing caches that call this
on hot file-operation paths.

Add a public delete(key) to LruCache and call it from TtlLruCache.get when
an entry is expired, so the stale entry is evicted immediately as the
comment promises. Add a vitest case (fake timers) asserting the expired
key is gone from the underlying store and no longer survives eviction.
@github-actions github-actions Bot added the v1 Legacy TypeScript line (0.x) — v1 branch, maintenance only label Jun 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v1 Legacy TypeScript line (0.x) — v1 branch, maintenance only

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant