Skip to content

feat(cache/unstable): add sliding expiration to TtlCache#7046

Merged
bartlomieju merged 2 commits intodenoland:mainfrom
tomas-zijdemans:ttl-cache-sliding
Mar 27, 2026
Merged

feat(cache/unstable): add sliding expiration to TtlCache#7046
bartlomieju merged 2 commits intodenoland:mainfrom
tomas-zijdemans:ttl-cache-sliding

Conversation

@tomas-zijdemans
Copy link
Copy Markdown
Contributor

@tomas-zijdemans tomas-zijdemans commented Mar 13, 2026

NOTE: Builds on #7065

Sliding expiration: entries can now stay alive as long as they're being accessed, with an optional hard deadline. Useful for sessions or rate-limit windows.

@tomas-zijdemans tomas-zijdemans requested a review from kt3k as a code owner March 13, 2026 11:56
@github-actions github-actions Bot added the cache label Mar 13, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented Mar 13, 2026

Codecov Report

❌ Patch coverage is 98.07692% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 94.41%. Comparing base (21ba810) to head (0b7946a).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
cache/ttl_cache.ts 98.07% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main    #7046   +/-   ##
=======================================
  Coverage   94.41%   94.41%           
=======================================
  Files         630      630           
  Lines       50435    50486   +51     
  Branches     8928     8946   +18     
=======================================
+ Hits        47616    47666   +50     
  Misses       2249     2249           
- Partials      570      571    +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions github-actions Bot added http and removed http labels Mar 15, 2026
Copy link
Copy Markdown
Member

@bartlomieju bartlomieju left a comment

Choose a reason for hiding this comment

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

The core feature (sliding expiration) is well-implemented and thoroughly tested. However, this PR bundles several breaking changes with a new feature. I'd recommend splitting it:

  1. One PR for the breaking cleanup (set() options object, delete/clear behavior, validation)
  2. One PR for the sliding expiration feature on top

Some specific points:

Breaking change: set() signature

The positional ttl parameter is replaced by an options object. Even for an unstable API, this should be called out explicitly as a breaking change.

clear() behavior change

clear() now calls onEject for every entry (swallowing intermediate errors, re-throwing the first). Previously it didn't call onEject at all. This could surprise existing users whose onEject has side effects.

delete() call order change

onEject now fires after the entry is removed from the map (previously it fired before). The test "entry is fully removed before onEject fires" validates this, but it's a subtle breaking change for any onEject callback that inspects the cache.

has() resetting TTL is debatable

In many caching libraries, only get() resets the sliding timer. has() is typically a read-only probe — having it keep entries alive indefinitely could cause unexpected behavior.

Third type parameter ergonomics

TtlCache<string, number, true> is verbose. A factory method or subclass might be cleaner than the redundancy between the type parameter and constructor option.

absoluteExpiration silently ignored without slidingExpiration

Passing absoluteExpiration on a non-sliding cache is only a compile-time error. At runtime it's quietly ignored with no validation or warning.

Stale commits in history

The PR contains two unrelated commits (feat(http): stabilize ServerSentEventParseStream and its revert) that should be rebased out before merge.

@tomas-zijdemans
Copy link
Copy Markdown
Contributor Author

Split out the breaking changes as requested: #7065

Once that is merged I will update this PR to build on it.

When `slidingExpiration: true` is passed to the constructor, each
`get()` call resets the entry's TTL. An optional per-entry
`absoluteExpiration` caps how far the sliding window can extend.

`has()` is a read-only probe and does not reset the TTL.

Made-with: Cursor
@tomas-zijdemans
Copy link
Copy Markdown
Contributor Author

Updated the PR, it now cleanly applies only the new sliding expiration feature

@bartlomieju
Copy link
Copy Markdown
Member

The third type parameter Sliding feels like it adds more friction than safety. Users have to write the true twice — once in the type and once in the constructor:

const cache = new TtlCache<string, number, true>(100, {
  slidingExpiration: true,
});

TypeScript can't infer the class-level generic from the constructor options, so this redundancy is unavoidable with the current design.

A few alternatives worth considering:

  1. SubclassSlidingTtlCache<K, V> that always has sliding behavior and accepts absoluteExpiration in set(). Clean separation, no extra type param.

  2. FactoryTtlCache.sliding<K, V>(100) returning a properly-typed instance.

  3. Drop the type param entirely — accept absoluteExpiration at runtime regardless. It's already silently ignored when sliding is off, so the type-level guard isn't buying much beyond catching a niche mistake at compile time.

Any of these would keep the API at two type params, which is much more ergonomic. Since this is still unstable, now's the time to get the shape right.

@tomas-zijdemans
Copy link
Copy Markdown
Contributor Author

Agree, that makes a lot of sense. I decided for option 3 (dropping the type parameter) with runtime validation.

@bartlomieju
Copy link
Copy Markdown
Member

Looks good, nice work! One minor nit for a follow-up:

The absoluteExpiration validation in set() happens after the entry is already inserted into the map (and the timeout scheduled). If someone passes an invalid value like NaN or -1, the RangeError fires but the entry is already live in the cache. Moving the validation up next to the ttl check (before any state mutation) would keep things consistent.

Not blocking — happy to land this as-is and fix in a follow-up.

@bartlomieju bartlomieju merged commit 008db8d into denoland:main Mar 27, 2026
19 checks passed
@tomas-zijdemans
Copy link
Copy Markdown
Contributor Author

Good catch. Sorry about that! Fix included here: #7070

tomas-zijdemans added a commit to tomas-zijdemans/std_cb that referenced this pull request Mar 30, 2026
…7046)

Sliding expiration: entries can now stay alive as long as they're being accessed, with an optional hard deadline. Useful for sessions or rate-limit windows.
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.

2 participants