Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ const repoDirApiContentsQueryOptions = (
export const Route = createFileRoute(
'/$libraryId/$version/docs/framework/$framework/examples/$',
)({
component: RouteComponent,
validateSearch: v.object({
path: v.optional(v.string()),
panel: v.optional(v.string()),
Expand Down Expand Up @@ -125,6 +124,32 @@ export const Route = createFileRoute(
}),
}
},
component: RouteComponent,
headers: ({ params }) => {
const { version, libraryId } = params
const library = getLibrary(libraryId)

const isLatestVersion =
version === 'latest' ||
version === library.latestVersion ||
version === library.latestBranch

if (isLatestVersion) {
return {
'cache-control': 'public, max-age=60, must-revalidate',
'cdn-cache-control':
'max-age=600, stale-while-revalidate=3600, durable',
vary: 'Accept-Encoding',
}
}

return {
'cache-control': 'public, max-age=3600, must-revalidate',
'cdn-cache-control':
'max-age=86400, stale-while-revalidate=604800, durable',
vary: 'Accept-Encoding',
}
},
staleTime: 1000 * 60 * 5, // 5 minutes
})

Expand Down
33 changes: 28 additions & 5 deletions src/utils/cache.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import LRUCache from 'lru-cache'

declare global {
var docCache: LRUCache<string, unknown>
var docStaleCache: LRUCache<string, unknown>
}

const docCache =
Expand All @@ -12,20 +13,42 @@ const docCache =
ttl: process.env.NODE_ENV === 'production' ? 1 : 1000000,
}))

const docStaleCache =
globalThis.docStaleCache ||
(globalThis.docStaleCache = new LRUCache<string, unknown>({
max: 300,
}))

export async function fetchCached<T>(opts: {
fn: () => Promise<T>
key: string
ttl: number
staleOnError?: boolean
}): Promise<T> {
if (docCache.has(opts.key)) {
return docCache.get(opts.key) as T
}

const result = await opts.fn()
try {
const result = await opts.fn()

docCache.set(opts.key, result, {
ttl: opts.ttl,
})
docCache.set(opts.key, result, {
ttl: opts.ttl,
})

return result
if (opts.staleOnError) {
docStaleCache.set(opts.key, result)
}

return result
} catch (error) {
if (opts.staleOnError && docStaleCache.has(opts.key)) {
console.warn(
`[fetchCached] Serving stale value for key '${opts.key}' after fetch error`,
)
return docStaleCache.get(opts.key) as T
}
Comment on lines +45 to +50
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Stale fallback does not suppress repeated upstream failures.

When fallback is used, the stale value is returned but not written back to docCache. After TTL expiry, each request still re-hits opts.fn() and fails again, which can amplify external outages.

Proposed fix
   } catch (error) {
     if (opts.staleOnError && docStaleCache.has(opts.key)) {
+      const staleValue = docStaleCache.get(opts.key) as T
+      // Short rehydrate window to avoid hammering failing upstreams
+      docCache.set(opts.key, staleValue, {
+        ttl: Math.min(opts.ttl, 30_000),
+      })
       console.warn(
         `[fetchCached] Serving stale value for key '${opts.key}' after fetch error`,
       )
-      return docStaleCache.get(opts.key) as T
+      return staleValue
     }

     throw error
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (opts.staleOnError && docStaleCache.has(opts.key)) {
console.warn(
`[fetchCached] Serving stale value for key '${opts.key}' after fetch error`,
)
return docStaleCache.get(opts.key) as T
}
if (opts.staleOnError && docStaleCache.has(opts.key)) {
const staleValue = docStaleCache.get(opts.key) as T
// Short rehydrate window to avoid hammering failing upstreams
docCache.set(opts.key, staleValue, {
ttl: Math.min(opts.ttl, 30_000),
})
console.warn(
`[fetchCached] Serving stale value for key '${opts.key}' after fetch error`,
)
return staleValue
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/cache.server.ts` around lines 45 - 50, When serving a stale
fallback in fetchCached (i.e. when opts.staleOnError is true and
docStaleCache.has(opts.key)), write that stale value back into the primary cache
(docCache) with a refreshed TTL instead of only returning it; update
docCache.set(opts.key, staleValue) (and any expiration metadata your cache uses)
so subsequent requests hit the cache until the refreshed TTL expires and don’t
immediately re-invoke opts.fn(), preventing repeated upstream failures. Ensure
you reference opts.key, docStaleCache, docCache, fetchCached, and opts.fn when
making this change.


throw error
}
}
46 changes: 44 additions & 2 deletions src/utils/documents.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ export async function fetchRepoFile(
const file = await fetchCached({
key,
ttl,
staleOnError: true,
fn: async () => {
const maxDepth = 4
let currentDepth = 1
Expand Down Expand Up @@ -423,6 +424,7 @@ export function fetchApiContents(
return fetchCached({
key: `${repoPair}:${branch}:${startingPath}`,
ttl: isDev ? 1 : 10 * 60 * 1000, // 10 minute
staleOnError: true,
fn: () => {
return isDev
? fetchApiContentsFs(repoPair, startingPath)
Expand Down Expand Up @@ -541,10 +543,16 @@ async function fetchApiContentsRemote(
branch: string,
startingPath: string,
): Promise<Array<GitHubFileNode> | null> {
const githubToken = env.GITHUB_AUTH_TOKEN
const hasConfiguredGitHubToken =
Boolean(githubToken) && githubToken !== 'USE_A_REAL_KEY_IN_PRODUCTION'

const fetchOptions: RequestInit = {
headers: {
'X-GitHub-Api-Version': '2022-11-28',
Authorization: `Bearer ${env.GITHUB_AUTH_TOKEN}`,
...(hasConfiguredGitHubToken
? { Authorization: `Bearer ${githubToken}` }
: {}),
},
}
const res = await fetch(
Expand All @@ -556,8 +564,42 @@ async function fetchApiContentsRemote(
if (res.status === 404) {
return null
}

const githubRequestId = res.headers.get('x-github-request-id')
const rateLimitLimit = res.headers.get('x-ratelimit-limit')
const rateLimitRemaining = res.headers.get('x-ratelimit-remaining')
const rateLimitReset = res.headers.get('x-ratelimit-reset')

let errorBody = ''
try {
errorBody = (await res.text()).replace(/\s+/g, ' ').trim()
} catch {
// Ignore parse failures for error response body
}

if (res.status === 403) {
console.error('[GitHub API] 403 while fetching repository contents', {
repo,
branch,
startingPath,
hasConfiguredGitHubToken,
githubRequestId,
rateLimitLimit,
rateLimitRemaining,
rateLimitReset,
errorBody: errorBody.slice(0, 500),
})
}

const hint =
res.status === 403
? rateLimitRemaining === '0'
? 'GitHub rate limit exceeded.'
: 'GitHub forbidden. Check token permissions/access.'
: 'GitHub request failed.'

throw new Error(
`Failed to fetch repo contents for ${repo}/${branch}/${startingPath}: Status is ${res.statusText} - ${res.status}`,
`${hint} Failed to fetch repo contents for ${repo}/${branch}/${startingPath}: Status is ${res.statusText} - ${res.status}. requestId=${githubRequestId ?? 'unknown'} rateLimitRemaining=${rateLimitRemaining ?? 'unknown'} rateLimitLimit=${rateLimitLimit ?? 'unknown'} rateLimitReset=${rateLimitReset ?? 'unknown'}${errorBody ? ` body=${errorBody.slice(0, 500)}` : ''}`,
)
}

Expand Down
Loading