Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
@@ -1,10 +1,5 @@
import { isNotFound, notFound, createFileRoute } from '@tanstack/react-router'
import {
queryOptions,
useQuery,
useQueryClient,
useSuspenseQuery,
} from '@tanstack/react-query'
import { queryOptions, useQuery, useQueryClient } from '@tanstack/react-query'
import React from 'react'
import { DocTitle } from '~/components/DocTitle'
import { Framework, getBranch, getLibrary } from '~/libraries'
Expand Down Expand Up @@ -52,7 +47,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 All @@ -62,6 +56,9 @@ export const Route = createFileRoute(
const library = getLibrary(params.libraryId)
const branch = getBranch(library, params.version)
const examplePath = [params.framework, params._splat].join('/')
const fallbackPath =
path ||
getExampleStartingPath(params.framework as Framework, params.libraryId)

// Used to tell the github contents api where to start looking for files in the target repository
const repoStartingDirPath = `examples/${examplePath}`
Expand All @@ -80,9 +77,7 @@ export const Route = createFileRoute(
// It's either the selected path in the search params or a default we can derive
// i.e. app.tsx, main.tsx, src/routes/__root.tsx, etc.
// This value is not absolutely guaranteed to be available, so further resolution may be necessary
const explorerCandidateStartingFileName =
path ||
getExampleStartingPath(params.framework as Framework, params.libraryId)
const explorerCandidateStartingFileName = fallbackPath

// Using the fetched contents, get the actual starting file-path for the explorer
// The `explorerCandidateStartingFileName` is used for matching, but the actual file-path may differ
Expand All @@ -99,15 +94,29 @@ export const Route = createFileRoute(
fileQueryOptions(library.repo, branch, currentPath),
)

return { repoStartingDirPath, currentPath }
return {
repoStartingDirPath,
currentPath,
githubContentsAvailable: true,
}
} catch (error) {
const isNotFoundError =
isNotFound(error) ||
(error && typeof error === 'object' && 'isNotFound' in error)
if (isNotFoundError) {
throw notFound()
}
throw error

console.warn(
`Failed to fetch example contents for ${library.repo}@${branch}:${repoStartingDirPath}`,
error,
)

return {
repoStartingDirPath,
currentPath: fallbackPath,
githubContentsAvailable: false,
}
}
},
head: ({ params }) => {
Expand All @@ -125,6 +134,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 All @@ -134,7 +169,8 @@ function RouteComponent() {
}

function PageComponent() {
const { repoStartingDirPath, currentPath } = Route.useLoaderData()
const { repoStartingDirPath, currentPath, githubContentsAvailable } =
Route.useLoaderData()

const navigate = Route.useNavigate()
const queryClient = useQueryClient()
Expand All @@ -149,9 +185,14 @@ function PageComponent() {
libraryId,
)

const { data: githubContents } = useSuspenseQuery(
repoDirApiContentsQueryOptions(library.repo, branch, repoStartingDirPath),
)
const { data: githubContents } = useQuery({
...repoDirApiContentsQueryOptions(
library.repo,
branch,
repoStartingDirPath,
),
enabled: githubContentsAvailable,
})

const [isDark, setIsDark] = React.useState(true)
const [deployDialogOpen, setDeployDialogOpen] = React.useState(false)
Expand Down Expand Up @@ -197,9 +238,10 @@ function PageComponent() {
})
}

const { data: currentCode } = useQuery(
fileQueryOptions(library.repo, branch, currentPath),
)
const { data: currentCode } = useQuery({
...fileQueryOptions(library.repo, branch, currentPath),
enabled: githubContentsAvailable,
})

const prefetchFileContent = React.useCallback(
(path: string) => {
Expand Down Expand Up @@ -312,19 +354,27 @@ function PageComponent() {
</DocTitle>
</div>
<div className="flex-1 lg:px-6 flex flex-col min-h-0">
<CodeExplorer
activeTab={activeTab as 'code' | 'sandbox'}
codeSandboxUrl={codeSandboxUrl}
currentCode={currentCode || ''}
currentPath={currentPath}
examplePath={examplePath}
githubContents={githubContents || undefined}
library={library}
prefetchFileContent={prefetchFileContent}
setActiveTab={setActiveTab}
setCurrentPath={setCurrentPath}
stackBlitzUrl={stackBlitzUrl}
/>
{githubContentsAvailable && githubContents ? (
<CodeExplorer
activeTab={activeTab as 'code' | 'sandbox'}
codeSandboxUrl={codeSandboxUrl}
currentCode={currentCode || ''}
currentPath={currentPath}
examplePath={examplePath}
githubContents={githubContents}
library={library}
prefetchFileContent={prefetchFileContent}
setActiveTab={setActiveTab}
setCurrentPath={setCurrentPath}
stackBlitzUrl={stackBlitzUrl}
/>
) : (
<div className="flex-1 rounded-xl border border-border/60 bg-background/70 p-6 text-sm text-muted-foreground">
The example source browser is temporarily unavailable. You can still
open this example on GitHub, StackBlitz, or CodeSandbox using the
links above.
</div>
)}
</div>
{deployProvider && (
<ExampleDeployDialog
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 @@ -230,7 +230,7 @@
}

// find all instances of markdown inline images
const markdownInlineImageRegex = /\!(\[([^\]]+)\]\(([^)]+)\))/g

Check warning on line 233 in src/utils/documents.server.ts

View workflow job for this annotation

GitHub Actions / PR

eslint(no-useless-escape)

Unnecessary escape character '!'
const inlineMarkdownImageMatches = text.matchAll(markdownInlineImageRegex)
for (const match of inlineMarkdownImageMatches) {
const [fullMatch, _, __, src] = match
Expand Down Expand Up @@ -287,6 +287,7 @@
const file = await fetchCached({
key,
ttl,
staleOnError: true,
fn: async () => {
const maxDepth = 4
let currentDepth = 1
Expand Down Expand Up @@ -423,6 +424,7 @@
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 @@
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 @@
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