cacheable - feat: support per-store TTL per operation#1656
Conversation
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
There was a problem hiding this comment.
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.
Codecov Report✅ All modified and coverable lines are covered by tests. 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. 🚀 New features to boost your workflow:
|
…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
There was a problem hiding this comment.
💡 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".
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
Summary
Closes #1654.
cacheableruns a two-layer cache (a primary in-memory store and an optional secondary distributed store). Until now, overriding thettlon 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
ttlaccepted byset,getOrSet, andwrapmay now be, in addition to the existingnumber | string, a per-store object:Each field accepts a number (ms) or a shorthand string (
'10s','5m', …). Behavior:number | string) is unchanged — applied to every store.primaryandsecondaryindependently.maxTtlcontinues to cap each store independently.BEFORE_SEThook override precedence is unchanged.Implementation
@cacheable/utils: added aPerStoreTtltype and a small, unit-testedresolvePerStoreTtlhelper (lives alongside the existing primary/secondarygetCascadingTtllogic). Widened the sharedttlfield onGetOrSetFunctionOptions,WrapFunctionOptions, and theCacheInstance/CacheSyncInstancesetsignatures so the object form can flow throughgetOrSet/wrapuntouched — these are type-only pass-throughs.cacheable:Cacheable.setnow resolves an explicit TTL per store and feeds each through the existing cascade/cap logic.getOrSet/wrapneed no logic change beyond their cache adapters wrapping the value as{ ttl }so the object isn't mistaken forSetOptions.SetOptions.ttlwidened andPerStoreTtlre-exported as public API.cacheableonly;cache-manageris untouched.Tests
packages/utils/test/ttl.test.ts: unit tests forresolvePerStoreTtl(scalar, shorthand, undefined, per-store object, partial object).packages/cacheable/test/ttl.test.ts: distinct-TTL-per-store, omitted-field fallback, scalar regression,maxTtlcapping per store, and per-store TTL viagetOrSetandwrap.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
cacheableREADME and updated the documented option types.🤖 Generated with Claude Code
https://claude.ai/code/session_01HjpVjtgWd5wyw17rsywLb8
Generated by Claude Code