Skip to content

fix(content-cache): force sync refresh on operator invalidation#933

Merged
tannerlinsley merged 1 commit into
mainfrom
taren/ecstatic-lichterman-ff8a0c
May 20, 2026
Merged

fix(content-cache): force sync refresh on operator invalidation#933
tannerlinsley merged 1 commit into
mainfrom
taren/ecstatic-lichterman-ff8a0c

Conversation

@tannerlinsley
Copy link
Copy Markdown
Member

@tannerlinsley tannerlinsley commented May 20, 2026

Summary

  • markGitHubContentStale / markDocsArtifactsStale use staleAt = new Date(0) as a "force refresh on next read" sentinel — that's what the admin Invalidate button and the GitHub push webhook both call. The admin UI even spells out the contract: "The next docs request for that repo will fetch fresh content from GitHub and rebuild derived artifacts."
  • The SWR readers in getCachedGitHubContent / getCachedDocsArtifact didn't honor it. When a row had positive cached content but was stale, they returned the cached value and fire-and-forgot a background refresh. On Netlify Functions that background promise is not guaranteed to land, the rendered page goes back into the CDN with stale content, and the next CDN revalidation pulls the same stale DB row — invalidation effectively never converges. Both reported symptoms (missing Deferred Hydration sidebar entry in Start docs and missing Astro Islands FAQ section in the guide body) trace to this single path: config.json and the markdown file both flow through fetchRepoFilegetCachedGitHubTextFilegetCachedGitHubContent.
  • Add isForciblyStale(staleAt) that recognizes the epoch sentinel and route forcibly-stale rows past the SWR branch into withPendingRefresh, which awaits a fresh origin fetch synchronously. Natural TTL expiry (24h positive / 15min negative) still SWRs as before. The stale-on-origin-error fallback inside withPendingRefresh is preserved, so a failed GitHub call right after invalidation still returns the previously cached value instead of erroring.

Test plan

  • pnpm test (tsc + lint) — clean for the touched file; pre-existing warnings elsewhere are unaffected
  • pnpm run test:smoke — passed locally (10/10) via the pre-commit hook
  • Manual: in production admin, click Invalidate on tanstack/router, then load /start/latest/docs/framework/react/deferred-hydration (or any updated doc) — confirm the page reflects the latest GitHub content within one origin revalidation cycle (rather than perpetuating the stale render)
  • Manual: confirm the Deferred Hydration entry appears in the Start sidebar after invalidation

Notes / follow-up

The Netlify CDN still has its own SWR windows in front of the SSR (max-age=600, stale-while-revalidate=3600 on latest docs pages; max-age=300, stale-while-revalidate=300 on the config server fn). With this fix the staleness is now bounded — once the CDN revalidates against origin, it will pick up fresh content. Before this fix the CDN kept re-caching stale renders indefinitely. Making invalidation feel instant requires also issuing a Netlify cache purge from the admin/webhook paths — happy to follow up with that as a separate PR once we decide on the token/IAM story.

Summary by CodeRabbit

  • Bug Fixes
    • Improved cache invalidation mechanism for GitHub content. Admin and webhook-triggered cache clears now properly prevent serving stale cached content, ensuring fresh data is fetched and served to users when needed.

Review Change Stack

markGitHubContentStale and markDocsArtifactsStale set staleAt to the
epoch as a "force refresh on next read" sentinel — used by the admin
invalidate button and the GitHub push webhook. The SWR readers in
getCachedGitHubContent and getCachedDocsArtifact ignored that intent:
when a row had positive cached content but was stale, they returned
the cached value and fire-and-forgot a background refresh. On Netlify
Functions the background promise often never lands, the rendered page
goes back into the CDN with stale content, and the next CDN
revalidation pulls the same stale row — invalidation effectively
never converges.

Add isForciblyStale(staleAt) that recognizes the epoch sentinel, and
route forcibly-stale rows past the SWR branch into withPendingRefresh
so the very next request awaits a fresh origin fetch. Natural TTL
expiry still SWRs as before. The stale-on-origin-error fallback at the
bottom of withPendingRefresh is preserved, so a failed GitHub call
after invalidation still returns the previously cached value instead
of erroring.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 20, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5e282275-c894-42dd-a439-115f9a6505f0

📥 Commits

Reviewing files that changed from the base of the PR and between 3a136f2 and 275beb6.

📒 Files selected for processing (1)
  • src/utils/github-content-cache.server.ts

📝 Walkthrough

Walkthrough

This PR adds epoch-based cache invalidation sentinel handling to the GitHub content and docs artifact cache layer. A new isForciblyStale helper identifies rows with staleAt <= 0 as admin-invalidated markers, and both cache functions now respect this sentinel by gating fresh-serve paths and disabling short-circuits in pending-refresh flows.

Changes

Cache invalidation sentinel

Layer / File(s) Summary
Forcibly stale sentinel predicate
src/utils/github-content-cache.server.ts
New isForciblyStale(staleAt) helper classifies rows with epoch-based or earlier timestamps as forcibly invalidated by admin/webhook actions.
GitHub content cache forcibly stale checks
src/utils/github-content-cache.server.ts
getCachedGitHubContent fast-path and pending-refresh path both check the sentinel; when set, cached values are not served, and the latest-value short-circuit is disabled even if the row is otherwise fresh.
Docs artifact cache forcibly stale checks
src/utils/github-content-cache.server.ts
getCachedDocsArtifact fast-path and pending-refresh path both check the sentinel; when set, artifacts are not served, and the latest-artifact short-circuit is disabled until the sentinel is cleared by fetching and re-upserting.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A rabbit guards the cache so dear,
With epoch sentinels, staleness crystal-clear,
When admins say "refresh!", the cache must obey,
No shortcuts allowed—purge the stale away!
Fresh from the source, or blocked from the store,
The sentinel whispers which paths to explore. 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(content-cache): force sync refresh on operator invalidation' directly describes the main change: forcing synchronous refresh when cache is invalidated by operators (admins/webhooks).
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch taren/ecstatic-lichterman-ff8a0c

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


Comment @coderabbitai help to get the list of available commands and usage tips.

@tannerlinsley tannerlinsley merged commit ca88f2c into main May 20, 2026
9 checks passed
@tannerlinsley tannerlinsley deleted the taren/ecstatic-lichterman-ff8a0c branch May 20, 2026 01:54
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.

1 participant