Skip to content

feat: support isEntryStillCurrentFn on in-memory-only loaders#420

Merged
kibertoad merged 3 commits into
mainfrom
claude/in-memory-staleness-probe-2sy0am
Jul 4, 2026
Merged

feat: support isEntryStillCurrentFn on in-memory-only loaders#420
kibertoad merged 3 commits into
mainfrom
claude/in-memory-staleness-probe-2sy0am

Conversation

@kibertoad

@kibertoad kibertoad commented Jul 4, 2026

Copy link
Copy Markdown
Owner

The staleness probe was hard-gated on an async cache tier: it only fired in
Loader/GroupLoader's async-hit branch and bumped freshness via
asyncCache.resetTtl/resetTtlFromGroup, so an in-memory-only loader in the
refresh window always paid a full data-source reload even when a cheap probe
could prove the cached value still current.

Make the existing pair of inMemoryCache.ttlLeftBeforeRefreshInMsecs and
isEntryStillCurrentFn a supported combination:

  • Relax assertStalenessCheckSupported to accept a probe when either the async
    cache has a refresh window (+ resetTtl, unchanged) or the in-memory cache has
    a refresh window.
  • Route the in-memory refresh-ahead through the probe via a new overridable
    scheduleInMemoryRefresh seam on the abstract caches; the loaders run the probe
    against the in-memory value and bump its TTL when current, falling back to the
    full background reload otherwise.
  • Add resetTtl/resetTtlFromGroup to the in-memory caches (and NoopCache) plus
    the SynchronousCache/SynchronousGroupCache interfaces.
  • Reuse the existing isKeyRefreshing / groupRefreshFlags stampede guards.

Precedence is unchanged: when an async cache has a refresh window it keeps
owning the probe, so existing configurations behave identically. Non-breaking.

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

Summary by CodeRabbit

  • New Features
    • Enabled conditional refresh with staleness checks for in-memory-only caching, extending entry TTL when still current.
    • Added per-entry/group in-memory TTL reset support to support TTL “bumping” without reloading.
  • Bug Fixes
    • When async and in-memory refresh windows are both configured, async refresh takes precedence.
    • Improved correctness and error handling for stale/missing/errored staleness probes, including safer refresh scheduling under concurrency.
  • Documentation
    • Updated README and changelog with clarified TTL behavior and configuration rules.
  • Tests
    • Added extensive coverage for in-memory staleness checks, concurrency, and race conditions.

The staleness probe was hard-gated on an async cache tier: it only fired in
Loader/GroupLoader's async-hit branch and bumped freshness via
asyncCache.resetTtl/resetTtlFromGroup, so an in-memory-only loader in the
refresh window always paid a full data-source reload even when a cheap probe
could prove the cached value still current.

Make the existing pair of inMemoryCache.ttlLeftBeforeRefreshInMsecs and
isEntryStillCurrentFn a supported combination:

- Relax assertStalenessCheckSupported to accept a probe when either the async
  cache has a refresh window (+ resetTtl, unchanged) or the in-memory cache has
  a refresh window.
- Route the in-memory refresh-ahead through the probe via a new overridable
  scheduleInMemoryRefresh seam on the abstract caches; the loaders run the probe
  against the in-memory value and bump its TTL when current, falling back to the
  full background reload otherwise.
- Add resetTtl/resetTtlFromGroup to the in-memory caches (and NoopCache) plus
  the SynchronousCache/SynchronousGroupCache interfaces.
- Reuse the existing isKeyRefreshing / groupRefreshFlags stampede guards.

Precedence is unchanged: when an async cache has a refresh window it keeps
owning the probe, so existing configurations behave identically. Non-breaking.

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

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

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

Next review available in: 50 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: 3248b3ec-69f3-497d-a885-6788a463531b

📥 Commits

Reviewing files that changed from the base of the PR and between 3e811ec and 4f879c3.

📒 Files selected for processing (1)
  • lib/AbstractCache.ts
📝 Walkthrough

Walkthrough

This PR extends isEntryStillCurrentFn support to in-memory-only loaders, adds TTL-reset methods to in-memory cache types, updates staleness-check validation and refresh scheduling, and adds loader/group-loader implementations plus documentation and tests for the new path.

Changes

In-memory staleness check feature

Layer / File(s) Summary
TTL reset contracts and cache implementations
lib/types/SyncDataSources.ts, lib/memory/InMemoryCache.ts, lib/memory/InMemoryGroupCache.ts, lib/memory/NoopCache.ts, test/memory/InMemoryCache.spec.ts, test/memory/InMemoryGroupCache.spec.ts
Adds resetTtl/resetTtlFromGroup to cache interfaces and implements them in the in-memory and noop cache classes, with tests covering extension and missing-entry cases.
Staleness check validation update
lib/AbstractCache.ts, test/Loader-staleness-check.spec.ts, test/GroupLoader-staleness-check.spec.ts, test/fakes/DummyGroupedCache.ts
Allows isEntryStillCurrentFn when either async or in-memory refresh windows are configured and updates constructor validation tests.
Overridable in-memory refresh scheduling hooks
lib/AbstractFlatCache.ts, lib/AbstractGroupCache.ts
Introduces scheduleInMemoryRefresh hooks and makes getAsyncOnlyResolved protected so subclasses can control the preemptive refresh path.
Loader in-memory refresh/bump implementation
lib/Loader.ts, test/Loader-in-memory-staleness-check.spec.ts
Adds guarded in-memory staleness probing, TTL reset, fallback reload, and concurrency/race coverage for Loader.
GroupLoader in-memory refresh/bump implementation
lib/GroupLoader.ts, test/GroupLoader-in-memory-staleness-check.spec.ts
Adds grouped in-memory staleness probing, TTL reset, fallback reload, and concurrency/race coverage for GroupLoader.
Documentation and changelog
README.md, CHANGELOG.md
Documents in-memory-only staleness-check behavior, precedence rules, and required reset methods.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Loader
  participant isEntryStillCurrentFn
  participant InMemoryCache
  participant DataSource
  Client->>Loader: get(key)
  Loader->>Loader: scheduleInMemoryRefresh(key)
  Loader->>isEntryStillCurrentFn: probe staleness
  isEntryStillCurrentFn-->>Loader: true/false
  alt still current
    Loader->>InMemoryCache: resetTtl(key)
  else stale
    Loader->>DataSource: loadFromLoaders(key)
    DataSource-->>Loader: fresh value
    Loader->>InMemoryCache: set(key, value)
  end
Loading
sequenceDiagram
  participant Client
  participant GroupLoader
  participant isEntryStillCurrentFn
  participant InMemoryGroupCache
  participant DataSource
  Client->>GroupLoader: get(key, group)
  GroupLoader->>GroupLoader: scheduleInMemoryRefresh(key, group)
  GroupLoader->>isEntryStillCurrentFn: probe staleness
  isEntryStillCurrentFn-->>GroupLoader: true/false
  alt still current
    GroupLoader->>InMemoryGroupCache: resetTtlFromGroup(key, group)
  else stale
    GroupLoader->>DataSource: loadFromLoaders(key)
    DataSource-->>GroupLoader: fresh value
    GroupLoader->>InMemoryGroupCache: set(key, group, value)
  end
Loading

Possibly related PRs

Suggested labels: minor

Suggested reviewers: andrewi-wd

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately summarizes the main change: adding isEntryStillCurrentFn support for in-memory-only loaders.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/in-memory-staleness-probe-2sy0am

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.

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (3)
lib/GroupLoader.ts (1)

126-149: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Same duplication concern as Loader.ts.

refreshOrBumpInMemoryTtl closely mirrors refreshOrBumpTtl (Lines 150-180) - same probe/fallback shape, differing only in the value source (getFromGroup vs already-resolved cachedValue) and reset target (resetTtlFromGroup vs asyncCache.resetTtlFromGroup). Consider factoring the shared probe+fallback logic into a helper, mirroring the suggestion made for Loader.ts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/GroupLoader.ts` around lines 126 - 149, `refreshOrBumpInMemoryTtl`
duplicates the same probe-and-fallback flow used by `refreshOrBumpTtl`, so
factor the shared logic into a helper to avoid maintaining two near-identical
paths. Keep the helper generic over the cached value source and TTL reset
target, then have `refreshOrBumpInMemoryTtl` and `refreshOrBumpTtl` delegate to
it using their respective cache access/reset methods and the existing
`isCurrentEntryTtlBumped` check.
lib/Loader.ts (1)

178-201: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider extracting shared probe/fallback logic.

refreshOrBumpInMemoryTtl largely duplicates the existing refreshOrBumpTtl (Lines 202-227): both call isCurrentEntryTtlBumped with an isEntryStillCurrentFn wrapper, then fall back to loadFromLoaders + inMemoryCache.set on staleness/failure. The only real differences are the source of cachedValue and the reset-TTL callback target.

A shared private helper taking cachedValue, resetTtl: () => Promise<boolean> as parameters would remove ~15 duplicated lines and keep future staleness-handling changes in one place.

♻️ Possible consolidation
-  private async refreshOrBumpInMemoryTtl(key: string, loadParams: LoadParams): Promise<void> {
-    const cachedValue = this.inMemoryCache.get(key)
-    if (
-      this.isEntryStillCurrentFn &&
-      // undefined means the entry vanished (expired/invalidated) between the read and the probe.
-      cachedValue !== undefined &&
-      (await this.isCurrentEntryTtlBumped(
-        key,
-        () => this.isEntryStillCurrentFn!(cachedValue, loadParams),
-        () => Promise.resolve(this.inMemoryCache.resetTtl(key)),
-      ))
-    ) {
-      // Still current - the in-memory TTL was bumped, nothing else to do.
-      return
-    }
-
-    // The entry is stale, the check failed, or the bump failed (entry expired/deleted meanwhile),
-    // so run the full background refresh from the data sources.
-    const freshValue = await this.loadFromLoaders(key, loadParams)
-    if (freshValue !== undefined) {
-      this.inMemoryCache.set(key, freshValue)
-    }
-  }
+  private async refreshOrBumpInMemoryTtl(key: string, loadParams: LoadParams): Promise<void> {
+    const cachedValue = this.inMemoryCache.get(key)
+    // undefined means the entry vanished (expired/invalidated) between the read and the probe.
+    if (cachedValue === undefined) {
+      return this.reloadAndCache(key, loadParams)
+    }
+    return this.refreshOrBumpTtlCommon(key, loadParams, cachedValue, () =>
+      Promise.resolve(this.inMemoryCache.resetTtl(key)),
+    )
+  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/Loader.ts` around lines 178 - 201, `refreshOrBumpInMemoryTtl` duplicates
the probe-and-fallback flow already used by `refreshOrBumpTtl`; extract that
shared logic into a private helper and have both methods delegate to it. Keep
the helper centered around `isCurrentEntryTtlBumped`, `loadFromLoaders`, and the
cache write path, while parameterizing the `cachedValue` source and the
`resetTtl` callback for `inMemoryCache`.
test/memory/InMemoryGroupCache.spec.ts (1)

321-347: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for null cached values in resetTtlFromGroup.

The implementation explicitly treats a cached null as a valid, extendable entry (distinct from undefined), but no test exercises that branch here.

✅ Suggested additional test
     it('returns false when the entry does not exist in an existing group', () => {
       const cache = new InMemoryGroupCache({ ttlInMsecs: 1000 })
       cache.setForGroup('other', 'value', 'group1')

       expect(cache.resetTtlFromGroup('missing', 'group1')).toBe(false)
     })
+
+    it('extends the ttl of an existing entry cached as null', () => {
+      const cache = new InMemoryGroupCache({ ttlInMsecs: 1000 })
+      cache.setForGroup('key', null, 'group1')
+
+      expect(cache.resetTtlFromGroup('key', 'group1')).toBe(true)
+    })
   })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/memory/InMemoryGroupCache.spec.ts` around lines 321 - 347, Add a test in
InMemoryGroupCache.spec.ts for resetTtlFromGroup covering a cached null value as
a valid entry. Use InMemoryGroupCache, setForGroup, and resetTtlFromGroup to
store null under a group, then verify the call returns true and the expiration
is extended, matching the existing existing-entry TTL behavior rather than the
missing-entry cases.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@lib/GroupLoader.ts`:
- Around line 126-149: `refreshOrBumpInMemoryTtl` duplicates the same
probe-and-fallback flow used by `refreshOrBumpTtl`, so factor the shared logic
into a helper to avoid maintaining two near-identical paths. Keep the helper
generic over the cached value source and TTL reset target, then have
`refreshOrBumpInMemoryTtl` and `refreshOrBumpTtl` delegate to it using their
respective cache access/reset methods and the existing `isCurrentEntryTtlBumped`
check.

In `@lib/Loader.ts`:
- Around line 178-201: `refreshOrBumpInMemoryTtl` duplicates the
probe-and-fallback flow already used by `refreshOrBumpTtl`; extract that shared
logic into a private helper and have both methods delegate to it. Keep the
helper centered around `isCurrentEntryTtlBumped`, `loadFromLoaders`, and the
cache write path, while parameterizing the `cachedValue` source and the
`resetTtl` callback for `inMemoryCache`.

In `@test/memory/InMemoryGroupCache.spec.ts`:
- Around line 321-347: Add a test in InMemoryGroupCache.spec.ts for
resetTtlFromGroup covering a cached null value as a valid entry. Use
InMemoryGroupCache, setForGroup, and resetTtlFromGroup to store null under a
group, then verify the call returns true and the expiration is extended,
matching the existing existing-entry TTL behavior rather than the missing-entry
cases.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a0ffcd67-ba89-4527-bfe3-1d8254b5c31f

📥 Commits

Reviewing files that changed from the base of the PR and between e497eab and ff79bf1.

📒 Files selected for processing (18)
  • CHANGELOG.md
  • README.md
  • lib/AbstractCache.ts
  • lib/AbstractFlatCache.ts
  • lib/AbstractGroupCache.ts
  • lib/GroupLoader.ts
  • lib/Loader.ts
  • lib/memory/InMemoryCache.ts
  • lib/memory/InMemoryGroupCache.ts
  • lib/memory/NoopCache.ts
  • lib/types/SyncDataSources.ts
  • test/GroupLoader-in-memory-staleness-check.spec.ts
  • test/GroupLoader-staleness-check.spec.ts
  • test/Loader-in-memory-staleness-check.spec.ts
  • test/Loader-staleness-check.spec.ts
  • test/fakes/DummyGroupedCache.ts
  • test/memory/InMemoryCache.spec.ts
  • test/memory/InMemoryGroupCache.spec.ts

claude added 2 commits July 4, 2026 18:35
…lyResolved

The new in-memory refresh path issued its stale-fallback reload via a raw
loadFromLoaders + inMemoryCache.set/setForGroup, outside the runningLoads /
groupLoads registry. That bypassed the invalidation fence and dedup that the
blind refresh path relies on, so:

- an entry invalidated (invalidateCacheFor) while the fallback reload was in
  flight got resurrected after the fact;
- a concurrent forceSetValue / forceSetValueForGroup was clobbered by the older
  in-flight snapshot;
- the fallback reload was not deduplicated against a concurrent cache-miss load
  (double fetch).

Route the fallback through the existing getAsyncOnlyResolved machinery (now
protected) instead: it registers the reload in runningLoads/groupLoads, so the
result is deduped and fenced by invalidateCacheFor / forceSetValue exactly like
every other load. Also report resetTtl rejections against the tier that owns the
refresh window (asyncCache ?? inMemoryCache) so the error handler never
dereferences an undefined asyncCache on the in-memory-only path.

Adds regression tests covering invalidation and forceSetValue racing an
in-flight fallback reload for both Loader and GroupLoader.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WCqiRPk8mruEJfYdhZ2BSk
The method is now shared between the async and in-memory refresh paths (the
reset target is injected via runResetTtl), so the summary no longer refers to
"async cache TTL" specifically.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WCqiRPk8mruEJfYdhZ2BSk
@kibertoad kibertoad added the patch label Jul 4, 2026
@kibertoad kibertoad merged commit 7e2d588 into main Jul 4, 2026
5 of 6 checks passed
@kibertoad kibertoad deleted the claude/in-memory-staleness-probe-2sy0am branch July 4, 2026 18:41
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