Skip to content

Commit 86dc04d

Browse files
authored
perf(workspace): server-prefetch home, knowledge, tables, and files list pages (#5196)
1 parent b212a5d commit 86dc04d

11 files changed

Lines changed: 411 additions & 14 deletions

File tree

apps/sim/app/workspace/[workspaceId]/files/page.tsx

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { Suspense } from 'react'
2+
import { dehydrate, HydrationBoundary } from '@tanstack/react-query'
23
import type { Metadata } from 'next'
4+
import { getQueryClient } from '@/app/_shell/providers/get-query-client'
5+
import { prefetchFilesBrowser } from '@/app/workspace/[workspaceId]/files/prefetch'
36
import { Files } from './files'
47
import FilesLoading from './loading'
58

@@ -15,10 +18,17 @@ export const metadata: Metadata = {
1518
* table headers) so a suspend never shows a blank frame; the route-level
1619
* `loading.tsx` covers the navigation/chunk-load transition the same way.
1720
*/
18-
export default function FilesPage() {
21+
export default async function FilesPage({ params }: { params: Promise<{ workspaceId: string }> }) {
22+
const { workspaceId } = await params
23+
24+
const queryClient = getQueryClient()
25+
await prefetchFilesBrowser(queryClient, workspaceId)
26+
1927
return (
20-
<Suspense fallback={<FilesLoading />}>
21-
<Files />
22-
</Suspense>
28+
<HydrationBoundary state={dehydrate(queryClient)}>
29+
<Suspense fallback={<FilesLoading />}>
30+
<Files />
31+
</Suspense>
32+
</HydrationBoundary>
2333
)
2434
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import type { QueryClient } from '@tanstack/react-query'
2+
import type { WorkspaceFileFolderApi } from '@/lib/api/contracts/workspace-file-folders'
3+
import type { ListWorkspaceFilesResponse } from '@/lib/api/contracts/workspace-files'
4+
import { prefetchInternalJson } from '@/app/workspace/[workspaceId]/lib/prefetch-internal-fetch'
5+
import { workspaceFileFolderKeys } from '@/hooks/queries/workspace-file-folders'
6+
import { workspaceFilesKeys } from '@/hooks/queries/workspace-files'
7+
8+
/**
9+
* Prefetches the Files browser's two lists — workspace files and file folders —
10+
* under the same query keys their client hooks (`useWorkspaceFiles`,
11+
* `useWorkspaceFileFolders`) use (scope `active`), so the browser paints
12+
* populated on first render.
13+
*
14+
* Both payloads carry `Date` fields, so they go through their routes and cache
15+
* the serialized wire shape — see {@link prefetchInternalJson}.
16+
*/
17+
export async function prefetchFilesBrowser(
18+
queryClient: QueryClient,
19+
workspaceId: string
20+
): Promise<void> {
21+
await Promise.all([
22+
queryClient.prefetchQuery({
23+
queryKey: workspaceFilesKeys.list(workspaceId, 'active'),
24+
queryFn: async () => {
25+
const data = await prefetchInternalJson<ListWorkspaceFilesResponse>(
26+
`/api/workspaces/${workspaceId}/files?scope=active`
27+
)
28+
return data.success ? data.files : []
29+
},
30+
staleTime: 30 * 1000,
31+
}),
32+
queryClient.prefetchQuery({
33+
queryKey: workspaceFileFolderKeys.list(workspaceId, 'active'),
34+
queryFn: async () => {
35+
const data = await prefetchInternalJson<{ folders?: WorkspaceFileFolderApi[] }>(
36+
`/api/workspaces/${workspaceId}/files/folders?scope=active`
37+
)
38+
return data.folders ?? []
39+
},
40+
staleTime: 30 * 1000,
41+
}),
42+
])
43+
}
Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,30 @@
11
import { Suspense } from 'react'
2+
import { dehydrate, HydrationBoundary } from '@tanstack/react-query'
23
import type { Metadata } from 'next'
34
import { getSession } from '@/lib/auth'
5+
import { getQueryClient } from '@/app/_shell/providers/get-query-client'
6+
import { prefetchHomeLists } from '@/app/workspace/[workspaceId]/home/prefetch'
47
import { Home } from './home'
58
import { HomeFallback } from './home-fallback'
69

710
export const metadata: Metadata = {
811
title: 'New chat',
912
}
1013

11-
export default async function HomePage() {
14+
export default async function HomePage({ params }: { params: Promise<{ workspaceId: string }> }) {
15+
const { workspaceId } = await params
16+
17+
const queryClient = getQueryClient()
18+
const listsPrefetch = prefetchHomeLists(queryClient, workspaceId)
19+
1220
const session = await getSession()
21+
await listsPrefetch
22+
1323
return (
14-
<Suspense fallback={<HomeFallback />}>
15-
<Home userName={session?.user?.name} userId={session?.user?.id} />
16-
</Suspense>
24+
<HydrationBoundary state={dehydrate(queryClient)}>
25+
<Suspense fallback={<HomeFallback />}>
26+
<Home userName={session?.user?.name} userId={session?.user?.id} />
27+
</Suspense>
28+
</HydrationBoundary>
1729
)
1830
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import type { QueryClient } from '@tanstack/react-query'
2+
import type { FolderApi } from '@/lib/api/contracts'
3+
import type { ListWorkspaceFilesResponse } from '@/lib/api/contracts/workspace-files'
4+
import { prefetchInternalJson } from '@/app/workspace/[workspaceId]/lib/prefetch-internal-fetch'
5+
import { FOLDER_LIST_STALE_TIME, mapFolder } from '@/hooks/queries/folders'
6+
import { folderKeys } from '@/hooks/queries/utils/folder-keys'
7+
import { workspaceFilesKeys } from '@/hooks/queries/workspace-files'
8+
9+
/**
10+
* Prefetches the home page's secondary lists — folders and workspace files —
11+
* under the same query keys their client hooks (`useFolders`,
12+
* `useWorkspaceFiles`) use, so the home view paints populated on first render.
13+
*
14+
* The workflow list (`workflowKeys.list(ws, 'active')`) is already hydrated by
15+
* the workspace sidebar prefetch and is intentionally not repeated here.
16+
*
17+
* Folders are fetched through the route and mapped with the same `mapFolder`
18+
* the hook applies, matching its cached shape (string dates → `Date`). Files
19+
* carry `Date` fields, so they go through the route and cache the serialized
20+
* wire shape — see {@link prefetchInternalJson}.
21+
*/
22+
export async function prefetchHomeLists(
23+
queryClient: QueryClient,
24+
workspaceId: string
25+
): Promise<void> {
26+
await Promise.all([
27+
queryClient.prefetchQuery({
28+
queryKey: folderKeys.list(workspaceId, 'active'),
29+
queryFn: async () => {
30+
const { folders } = await prefetchInternalJson<{ folders?: FolderApi[] }>(
31+
`/api/folders?workspaceId=${workspaceId}&scope=active`
32+
)
33+
return (folders ?? []).map(mapFolder)
34+
},
35+
staleTime: FOLDER_LIST_STALE_TIME,
36+
}),
37+
queryClient.prefetchQuery({
38+
queryKey: workspaceFilesKeys.list(workspaceId, 'active'),
39+
queryFn: async () => {
40+
const data = await prefetchInternalJson<ListWorkspaceFilesResponse>(
41+
`/api/workspaces/${workspaceId}/files?scope=active`
42+
)
43+
return data.success ? data.files : []
44+
},
45+
staleTime: 30 * 1000,
46+
}),
47+
])
48+
}
Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,26 @@
1+
import { dehydrate, HydrationBoundary } from '@tanstack/react-query'
12
import type { Metadata } from 'next'
3+
import { getQueryClient } from '@/app/_shell/providers/get-query-client'
4+
import { prefetchKnowledgeBases } from '@/app/workspace/[workspaceId]/knowledge/prefetch'
25
import { Knowledge } from './knowledge'
36

47
export const metadata: Metadata = {
58
title: 'Knowledge Base',
69
}
710

8-
export default Knowledge
11+
export default async function KnowledgePage({
12+
params,
13+
}: {
14+
params: Promise<{ workspaceId: string }>
15+
}) {
16+
const { workspaceId } = await params
17+
18+
const queryClient = getQueryClient()
19+
await prefetchKnowledgeBases(queryClient, workspaceId)
20+
21+
return (
22+
<HydrationBoundary state={dehydrate(queryClient)}>
23+
<Knowledge />
24+
</HydrationBoundary>
25+
)
26+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import type { QueryClient } from '@tanstack/react-query'
2+
import type { KnowledgeBaseData } from '@/lib/api/contracts/knowledge'
3+
import { prefetchInternalJson } from '@/app/workspace/[workspaceId]/lib/prefetch-internal-fetch'
4+
import { knowledgeKeys } from '@/hooks/queries/kb/knowledge'
5+
6+
/**
7+
* Prefetches the workspace's knowledge-bases list under the same query key the
8+
* client `useKnowledgeBasesQuery` hook uses (scope `active`), so the list paints
9+
* populated on first render.
10+
*
11+
* The list carries `Date` fields, so it goes through the `/api/knowledge` route
12+
* and caches the serialized wire shape — see {@link prefetchInternalJson}.
13+
*/
14+
export async function prefetchKnowledgeBases(
15+
queryClient: QueryClient,
16+
workspaceId: string
17+
): Promise<void> {
18+
await queryClient.prefetchQuery({
19+
queryKey: knowledgeKeys.list(workspaceId, 'active'),
20+
queryFn: async () => {
21+
const result = await prefetchInternalJson<{ data: KnowledgeBaseData[] }>(
22+
`/api/knowledge?workspaceId=${workspaceId}&scope=active`
23+
)
24+
return result.data
25+
},
26+
staleTime: 60 * 1000,
27+
})
28+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { headers } from 'next/headers'
2+
import { getInternalApiBaseUrl } from '@/lib/core/utils/urls'
3+
4+
/**
5+
* Server-side GET against an internal `/api` route, forwarding the incoming
6+
* request's cookie so the route authenticates as the current user.
7+
*
8+
* List prefetches go through the route (rather than the data layer) when the
9+
* payload carries `Date` fields: `NextResponse.json` serializes them to the
10+
* string wire shape the client caches via `requestJson`, so the
11+
* server-hydrated entry byte-matches the client-fetched one through
12+
* dehydration. Calling the data layer directly would cache raw `Date` objects
13+
* and drift from that wire shape. Mirrors the settings/subscription prefetch.
14+
*/
15+
export async function prefetchInternalJson<T>(path: string): Promise<T> {
16+
const cookie = (await headers()).get('cookie')
17+
// boundary-raw-fetch: server-side RSC prefetch forwarding the session cookie to an internal API route; requestJson is client-only and cannot run here
18+
const response = await fetch(`${getInternalApiBaseUrl()}${path}`, {
19+
headers: cookie ? { cookie } : {},
20+
})
21+
if (!response.ok) {
22+
throw new Error(`Prefetch failed for ${path}: ${response.status}`)
23+
}
24+
return response.json() as Promise<T>
25+
}

0 commit comments

Comments
 (0)