Skip to content

feat: add cacheGet probe API and null-sentinel caching#486

Open
funkypenguin wants to merge 1 commit into
cedya77:devfrom
funkypenguin:fix/cache-probe-api
Open

feat: add cacheGet probe API and null-sentinel caching#486
funkypenguin wants to merge 1 commit into
cedya77:devfrom
funkypenguin:fix/cache-probe-api

Conversation

@funkypenguin
Copy link
Copy Markdown

Summary

Three related changes to cache hygiene in addon/lib/getCache.js:

  1. Null-sentinel cachingcacheWrap / cacheWrapGlobal now persist null/undefined results as a {__null_sentinel: true} entry. This prevents hot re-fetch loops when an upstream provider legitimately returns nothing (e.g. fanart for a series that doesn't have it). Sentinels expire per the result classifier's EMPTY_RESULT TTL, raised from 60s to 15min to match.

  2. In-flight deduplication race fix — previously the in-flight promise was registered after await redis.get(), so concurrent callers for the same key could each get past the has() check and start their own producer call. The whole flow is now wrapped in an IIFE async promise registered before any await.

  3. New cacheGet / cacheGetGlobal read-only probe API — returns the cached value or null on miss without invoking any producer or writing to Redis. Four call sites in addon/lib/getCatalog.ts (Trakt up-next and Trakt unwatched) had been using cacheWrap(key, async () => null, ttl) as a probe; those are migrated to cacheGet.

Why the migration matters

The probe pattern at those four sites was load-bearing before this change because nothing caused a sentinel write. With (1) in place, that same pattern poisons the cache:

  • Miss → producer returns null → sentinel is written
  • Next cacheWrap call meant to populate the key sees the sentinel → returns null without invoking its real producer
  • Trakt up-next / unwatched catalogs would stop updating until the sentinel TTL (5 min) expired

cacheGet keeps the read-only intent explicit and avoids that interaction. The JSDoc on cacheGet explicitly warns against the old probe pattern so future contributors don't re-introduce it.

Test plan

  • Trakt up-next catalog refreshes correctly: items appear on first load, persist across the 5-min cache window, and rebuild when activity changes
  • Trakt unwatched catalog behaves the same
  • Fanart misses (e.g. a series with no Fanart.tv backdrop) don't trigger continuous re-fetching of the same upstream — log volume on [Fanart] should drop for known-missing IDs
  • Concurrent requests for the same uncached meta key produce a single upstream provider call (dedup race fix)
  • No regression on the 215+ other cacheWrap* call sites in the codebase — they're all real producers, so the sentinel-write path is the desired behavior for them

🤖 Generated with Claude Code

Three related changes to cache hygiene:

1. cacheWrap / cacheWrapGlobal now persist null/undefined results as a
   `{__null_sentinel: true}` entry. This prevents hot re-fetch loops
   when an upstream provider legitimately returns nothing (e.g. fanart
   for a series that doesn't have it). Sentinels expire per the result
   classifier's EMPTY_RESULT TTL, raised from 60s to 15min to match.

2. Fix an in-flight deduplication race: previously the in-flight
   promise was registered AFTER `await redis.get()`, so concurrent
   callers for the same key could each get past the `has()` check and
   start their own producer call. Wrap the entire flow in an IIFE
   async promise and register it before any await.

3. Add cacheGet / cacheGetGlobal — read-only probe functions that
   return the cached value or null on miss WITHOUT invoking any
   producer or writing to Redis. Migrate the four call sites in
   getCatalog.ts (Trakt up-next, Trakt unwatched) that had been using
   `cacheWrap(key, async () => null, ttl)` as a probe.

The probe pattern at those four sites was load-bearing before this
change because nothing caused a sentinel write. With (1) in place,
that same pattern poisons the cache: on a miss the no-op producer
returns null, the sentinel is written, and the next cacheWrap call to
populate the key sees the sentinel and returns null without invoking
its real producer. Trakt up-next / unwatched catalogs would stop
updating until the sentinel TTL expired. cacheGet keeps the
read-only intent explicit and avoids that interaction.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

PR Guard

  • Missing template sections: ## linked issue, ## type of change, ## why this approach, ## testing, ## documentation, ## author checklist, ## ai usage disclosure
  • Non-trivial PRs must link an issue in the PR body.
  • Please complete the relevant checkboxes in the PR template.

Maintainers may still close PRs that do not match project direction or review capacity.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant