Skip to content

cacheable - feat: support per-store TTL per operation#1656

Merged
jaredwray merged 4 commits into
mainfrom
claude/cacheable-issue-1654-2zt4ez
Jun 17, 2026
Merged

cacheable - feat: support per-store TTL per operation#1656
jaredwray merged 4 commits into
mainfrom
claude/cacheable-issue-1654-2zt4ez

Conversation

@jaredwray

Copy link
Copy Markdown
Owner

Summary

Closes #1654.

cacheable runs a two-layer cache (a primary in-memory store and an optional secondary distributed store). Until now, overriding the ttl on a single operation applied that one value to both stores, so there was no way to say "keep this key 10s in memory but 5min in Redis."

This adds per-store, per-operation TTL: the ttl accepted by set, getOrSet, and wrap may now be, in addition to the existing number | string, a per-store object:

await cache.set('key', 'value', { ttl: { primary: '10s', secondary: '5m' } });

Each field accepts a number (ms) or a shorthand string ('10s', '5m', …). Behavior:

  • Scalar TTL (number | string) is unchanged — applied to every store.
  • A per-store object resolves primary and secondary independently.
  • An omitted field falls back to that store's own default TTL resolution (storage-adapter TTL, then the instance TTL), preserving the existing cascade.
  • maxTtl continues to cap each store independently.
  • The existing BEFORE_SET hook override precedence is unchanged.

Implementation

  • @cacheable/utils: added a PerStoreTtl type and a small, unit-tested resolvePerStoreTtl helper (lives alongside the existing primary/secondary getCascadingTtl logic). Widened the shared ttl field on GetOrSetFunctionOptions, WrapFunctionOptions, and the CacheInstance/CacheSyncInstance set signatures so the object form can flow through getOrSet/wrap untouched — these are type-only pass-throughs.
  • cacheable: Cacheable.set now resolves an explicit TTL per store and feeds each through the existing cascade/cap logic. getOrSet/wrap need no logic change beyond their cache adapters wrapping the value as { ttl } so the object isn't mistaken for SetOptions. SetOptions.ttl widened and PerStoreTtl re-exported as public API.
  • Scope is cacheable only; cache-manager is untouched.

Tests

  • packages/utils/test/ttl.test.ts: unit tests for resolvePerStoreTtl (scalar, shorthand, undefined, per-store object, partial object).
  • packages/cacheable/test/ttl.test.ts: distinct-TTL-per-store, omitted-field fallback, scalar regression, maxTtl capping per store, and per-store TTL via getOrSet and wrap.

All non-Redis tests pass locally with 100% coverage on the changed code. Redis-backed integration tests were not run here (no Docker/Redis in the environment); CI will exercise those.

Docs

Added a "Per-Store TTL per Operation" section to the cacheable README and updated the documented option types.

🤖 Generated with Claude Code

https://claude.ai/code/session_01HjpVjtgWd5wyw17rsywLb8


Generated by Claude Code

Allow the `ttl` on `set`, `getOrSet`, and `wrap` to accept a per-store
object `{ primary?, secondary? }` in addition to a number or shorthand
string. Each field is resolved independently so a single operation can
expire a key at different rates in the primary and secondary stores
(e.g. 10s in memory, 5m in Redis). Fields left undefined fall back to
that store's own default TTL resolution, and a scalar TTL keeps the
existing behavior of applying to both stores.

Adds a `PerStoreTtl` type and `resolvePerStoreTtl` helper to
`@cacheable/utils` and widens the shared option/adapter TTL types so the
object form flows through `getOrSet`/`wrap` to `Cacheable.set`, which is
the only place that interprets it. `maxTtl` continues to cap each store
independently.

Closes #1654

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HjpVjtgWd5wyw17rsywLb8

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

This pull request introduces support for per-store TTL overrides per operation, allowing multi-layer caches to have different expiration rates for primary and secondary stores. It updates the Cacheable class, getOrSet, and wrap methods to accept a PerStoreTtl object containing primary and secondary fields, and falls back to store defaults if fields are omitted. Feedback was provided to improve the robustness of the resolvePerStoreTtl utility by explicitly handling null and undefined inputs to prevent potential runtime errors.

Comment thread packages/utils/src/ttl.ts
@codecov

codecov Bot commented Jun 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (639e42c) to head (1ae44d1).

Additional details and impacted files
@@            Coverage Diff            @@
##              main     #1656   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           27        27           
  Lines         3069      3079   +10     
  Branches       693       684    -9     
=========================================
+ Hits          3069      3079   +10     

☔ View full report in Codecov by Harness.
📢 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.

…icit

Use an explicit early return for null/undefined inputs instead of
relying on shorthandToMilliseconds's lenient handling. Behavior is
unchanged (both already resolved to undefined), but the contract is now
clearer. Adds a null-input test to keep full coverage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HjpVjtgWd5wyw17rsywLb8

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ba8f7e2617

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/utils/src/memoize.ts Outdated
Comment thread packages/cacheable/src/index.ts
Comment thread packages/cacheable/src/index.ts
claude added 2 commits June 17, 2026 15:52
With per-store TTLs the secondary copy can be shorter-lived than the
primary. The tag snapshot previously expired with the secondary TTL, so
once the secondary copy lapsed the snapshot was gone while the primary
copy was still cached, and a later tag invalidation could no longer mark
that copy stale. Set the snapshot TTL to the longest-lived store copy
(undefined when either store never expires) so invalidation is honored
for as long as any copy of the value remains.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HjpVjtgWd5wyw17rsywLb8
…on types

The per-store TTL object is a Cacheable-only concept (primary/secondary
stores), but widening the shared WrapFunctionOptions/GetOrSetFunctionOptions
in @cacheable/utils also exposed it on single-store consumers such as
CacheableMemory.wrap, where the object is silently ignored.

Revert the public/sync option types in @cacheable/utils to number|string
and widen only the internal WrapOptions/GetOrSetOptions (plus the
CacheInstance adapter) so the object can still flow through to
Cacheable.set. Cacheable now defines its own wide WrapFunctionOptions/
GetOrSetFunctionOptions, so CacheableMemory.wrap rejects a per-store
object at compile time while Cacheable.wrap/getOrSet accept it.

Also document that a per-store primary TTL governs the write only; after
an L1 entry expires, backfill from the secondary follows the existing TTL
propagation rules.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HjpVjtgWd5wyw17rsywLb8
@jaredwray jaredwray changed the title feat(cacheable): support per-store TTL per operation cacheable - feat: support per-store TTL per operation Jun 17, 2026
@jaredwray
jaredwray merged commit 2debaff into main Jun 17, 2026
12 checks passed
@jaredwray
jaredwray deleted the claude/cacheable-issue-1654-2zt4ez branch June 17, 2026 16:48
@jaredwray jaredwray mentioned this pull request Jun 27, 2026
6 tasks
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.

Add support for per‑operation dynamic TTL per store in cacheable / cache-manager

3 participants