Skip to content

Commit 9863578

Browse files
committed
fix(sidebar): prefetch chats + workflows so cold loads don't flash skeletons
On a cold load (e.g. when the browser discards an idle tab and reloads), the persistent sidebar started with an empty React Query cache and client-fetched its chat + workflow lists, flashing loading skeletons. Prefetch both lists server-side in the workspace layout and hydrate them via HydrationBoundary, under the same query keys and mappers the client hooks use, so the sidebar paints populated on the first render. The prefetch runs concurrently with the existing org-settings fetch and never throws, so it adds no blocking work in the common case and falls back to client fetching on error.
1 parent 2ffc004 commit 9863578

3 files changed

Lines changed: 84 additions & 3 deletions

File tree

apps/sim/app/workspace/[workspaceId]/layout.tsx

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
1+
import { dehydrate, HydrationBoundary } from '@tanstack/react-query'
12
import { redirect } from 'next/navigation'
23
import { ToastProvider } from '@/components/emcn'
34
import { getSession } from '@/lib/auth'
5+
import { getQueryClient } from '@/app/_shell/providers/get-query-client'
46
import { ImpersonationBanner } from '@/app/workspace/[workspaceId]/components/impersonation-banner'
57
import { WorkspaceChrome } from '@/app/workspace/[workspaceId]/components/workspace-chrome'
8+
import {
9+
prefetchSidebarChats,
10+
prefetchSidebarWorkflows,
11+
} from '@/app/workspace/[workspaceId]/prefetch'
612
import { GlobalCommandsProvider } from '@/app/workspace/[workspaceId]/providers/global-commands-provider'
713
import { ProviderModelsLoader } from '@/app/workspace/[workspaceId]/providers/provider-models-loader'
814
import { SettingsLoader } from '@/app/workspace/[workspaceId]/providers/settings-loader'
@@ -11,15 +17,31 @@ import { WorkspaceScopeSync } from '@/app/workspace/[workspaceId]/providers/work
1117
import { BrandingProvider } from '@/ee/whitelabeling/components/branding-provider'
1218
import { getOrgWhitelabelSettings } from '@/ee/whitelabeling/org-branding'
1319

14-
export default async function WorkspaceLayout({ children }: { children: React.ReactNode }) {
20+
export default async function WorkspaceLayout({
21+
children,
22+
params,
23+
}: {
24+
children: React.ReactNode
25+
params: Promise<{ workspaceId: string }>
26+
}) {
1527
const session = await getSession()
1628
if (!session?.user) {
1729
redirect('/login')
1830
}
31+
32+
const { workspaceId } = await params
33+
const queryClient = getQueryClient()
34+
const sidebarPrefetch = Promise.all([
35+
prefetchSidebarWorkflows(queryClient, workspaceId),
36+
prefetchSidebarChats(queryClient, workspaceId),
37+
])
38+
1939
// The organization plugin is conditionally spread so TS can't infer activeOrganizationId on the base session type.
2040
const orgId = (session.session as { activeOrganizationId?: string } | null)?.activeOrganizationId
2141
const initialOrgSettings = orgId ? await getOrgWhitelabelSettings(orgId) : null
2242

43+
await sidebarPrefetch
44+
2345
return (
2446
<BrandingProvider initialOrgSettings={initialOrgSettings}>
2547
<ToastProvider>
@@ -30,7 +52,9 @@ export default async function WorkspaceLayout({ children }: { children: React.Re
3052
<ImpersonationBanner />
3153
<WorkspacePermissionsProvider>
3254
<WorkspaceScopeSync />
33-
<WorkspaceChrome>{children}</WorkspaceChrome>
55+
<HydrationBoundary state={dehydrate(queryClient)}>
56+
<WorkspaceChrome>{children}</WorkspaceChrome>
57+
</HydrationBoundary>
3458
</WorkspacePermissionsProvider>
3559
</div>
3660
</GlobalCommandsProvider>
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import type { QueryClient } from '@tanstack/react-query'
2+
import { headers } from 'next/headers'
3+
import { getInternalApiBaseUrl } from '@/lib/core/utils/urls'
4+
import { mapChat, mothershipChatKeys } from '@/hooks/queries/mothership-chats'
5+
import { workflowKeys } from '@/hooks/queries/utils/workflow-keys'
6+
import { mapWorkflow, WORKFLOW_LIST_STALE_TIME } from '@/hooks/queries/utils/workflow-list-query'
7+
8+
/** Forwards incoming request cookies so server-side API fetches authenticate correctly. */
9+
async function getForwardedHeaders(): Promise<Record<string, string>> {
10+
const h = await headers()
11+
const cookie = h.get('cookie')
12+
return cookie ? { cookie } : {}
13+
}
14+
15+
/**
16+
* Prefetches the workspace's workflow list under the same key and mapping as the client
17+
* `useWorkflows`/`useWorkflowMap` hooks, so the dehydrated data hydrates the sidebar.
18+
*/
19+
export function prefetchSidebarWorkflows(queryClient: QueryClient, workspaceId: string) {
20+
return queryClient.prefetchQuery({
21+
queryKey: workflowKeys.list(workspaceId, 'active'),
22+
queryFn: async () => {
23+
const fwdHeaders = await getForwardedHeaders()
24+
const baseUrl = getInternalApiBaseUrl()
25+
const response = await fetch(
26+
`${baseUrl}/api/workflows?workspaceId=${encodeURIComponent(workspaceId)}&scope=active`,
27+
{ headers: fwdHeaders }
28+
)
29+
if (!response.ok) throw new Error(`Workflows prefetch failed: ${response.status}`)
30+
const { data } = await response.json()
31+
return data.map(mapWorkflow)
32+
},
33+
staleTime: WORKFLOW_LIST_STALE_TIME,
34+
})
35+
}
36+
37+
/**
38+
* Prefetches the workspace's mothership chat list under the same key and mapping as the
39+
* client `useMothershipChats` hook, so the dehydrated data hydrates the sidebar.
40+
*/
41+
export function prefetchSidebarChats(queryClient: QueryClient, workspaceId: string) {
42+
return queryClient.prefetchQuery({
43+
queryKey: mothershipChatKeys.list(workspaceId),
44+
queryFn: async () => {
45+
const fwdHeaders = await getForwardedHeaders()
46+
const baseUrl = getInternalApiBaseUrl()
47+
const response = await fetch(
48+
`${baseUrl}/api/mothership/chats?workspaceId=${encodeURIComponent(workspaceId)}`,
49+
{ headers: fwdHeaders }
50+
)
51+
if (!response.ok) throw new Error(`Chats prefetch failed: ${response.status}`)
52+
const { data } = await response.json()
53+
return data.map(mapChat)
54+
},
55+
staleTime: 60 * 1000,
56+
})
57+
}

apps/sim/hooks/queries/mothership-chats.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ function parseChatResourcesResponse(value: unknown): { resources: MothershipReso
183183
}
184184
}
185185

186-
function mapChat(chat: MothershipChat): MothershipChatMetadata {
186+
export function mapChat(chat: MothershipChat): MothershipChatMetadata {
187187
const updatedAt = new Date(chat.updatedAt)
188188
return {
189189
id: chat.id,

0 commit comments

Comments
 (0)