Skip to content

feat(cache): support dynamic per-entry TTL via getMaxAge hook - #39

Merged
pi0 merged 2 commits into
mainfrom
feat/dynamic-ttl
Jul 7, 2026
Merged

pi0 merged 2 commits into
mainfrom
feat/dynamic-ttl

Conversation

@pi0x

@pi0x pi0x commented Jul 7, 2026 •

Copy link
Copy Markdown
Contributor

Closes #36.

Summary

TTL was fixed at creation time (maxAge / staleMaxAge). This adds a getMaxAge hook that derives an entry's lifetime from the value it resolved to, so the cache can honor per-entry expiry instead of a single global constant.

const getToken = defineCachedFunction(() => fetchToken(), {
  // cache each token for exactly its own lifetime (number shorthand for maxAge)
  getMaxAge: (entry) => entry.value?.expires_in,
});

Motivating cases: OAuth/API tokens with expires_in, upstream responses carrying their own Cache-Control: max-age, or content-derived freshness.

How it works

  • getMaxAge?: (entry) => number | { maxAge?, staleMaxAge? } | undefined runs after the resolver, before the entry is persisted (async-supported).
  • A bare number is shorthand for { maxAge }; return an object to also override the stale window.
  • The resolved values are stored on the entry (CacheEntry.maxAge / CacheEntry.staleMaxAge) and used for both the read-side freshness check (including the SWR stale window) and the storage TTL on write.
  • Missing field / undefined return β†’ falls back to the static option. If the hook throws, the entry is still cached with static options and the error is reported via onError.
  • expireCache() prefers the per-entry TTL when computing the remaining storage window.
  • Flows through to defineCachedHandler automatically (entry value is the ResponseCacheEntry).

Compatibility

Additive and fully backwards compatible β€” when getMaxAge is absent, behavior is unchanged.

Tests

New getMaxAge block covering: number-shorthand freshness derived from the value, persistence on the stored entry, fallback when the hook returns undefined, per-entry SWR stale window, hook-throws resilience, and expireCache respecting per-entry TTL. All 105 tests pass; typecheck clean.

πŸ€– Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Jul 7, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

@pi0x, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 17 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
βš™οΈ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1f53c43c-a4da-485d-a964-e068b6edd92b

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 0266ada and 4d042a2.

πŸ“’ Files selected for processing (5)
  • AGENTS.md
  • README.md
  • src/cache.ts
  • src/types.ts
  • test/index.test.ts
✨ Finishing Touches
πŸ§ͺ Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/dynamic-ttl

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.

Adds a `getMaxAge` option that runs after the resolver and returns
either a number (seconds, shorthand for `maxAge`) or
`{ maxAge?, staleMaxAge? }` to override the static options for that
specific entry. Resolved values are persisted on the entry and drive
both the read freshness check and the storage TTL, letting the cache
honor per-entry lifetimes (OAuth `expires_in`, upstream
`Cache-Control`, content-derived freshness).

Additive and backwards compatible β€” absent `getMaxAge` leaves behavior
unchanged. Flows through to `defineCachedHandler` automatically.

Closes #36

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@pi0x
pi0x force-pushed the feat/dynamic-ttl branch from 405b6b8 to 28f8580 Compare July 7, 2026 15:23
@pi0x pi0x changed the title feat(cache): support dynamic per-entry TTL via ttl hook feat(cache): support dynamic per-entry TTL via getMaxAge hook Jul 7, 2026
A negative or non-finite `getMaxAge` result (e.g. an already-expired
token's `expires_in - margin`) previously left `ttl <= 0`, which the
freshness check treated as "never expires" β€” pinning the entry as fresh
forever instead of not caching it. Clamp negatives to 0 (re-resolve every
access) and fall back to static options for non-finite values. Fix the
README example so it can't derive a negative TTL.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@pi0
pi0 merged commit b16418b into main Jul 7, 2026
5 checks passed
@pi0
pi0 deleted the feat/dynamic-ttl branch July 7, 2026 15:51
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.

Support dynamic per-entry TTL derived from the resolved value (getTtl)

2 participants