Skip to content

perf(workspace): server-prefetch home, knowledge, tables, and files list pages#5196

Merged
waleedlatif1 merged 1 commit into
stagingfrom
perf/workspace-list-prefetch
Jun 24, 2026
Merged

perf(workspace): server-prefetch home, knowledge, tables, and files list pages#5196
waleedlatif1 merged 1 commit into
stagingfrom
perf/workspace-list-prefetch

Conversation

@waleedlatif1

Copy link
Copy Markdown
Collaborator

Summary

  • Add RSC server-side prefetch + <HydrationBoundary> to the home, knowledge, tables, and files workspace list pages (previously only 2 of ~24 routes prefetched; the rest fetched on mount after hydration).
  • Each page's server wrapper awaits its prefetch before dehydrate() so the client hydrates with resolved data (the app has no ReactQueryStreamedHydration, so pending-query dehydration would be unreliable).
  • Shared prefetch-internal-fetch.ts helper centralizes the cookie-forwarded internal-route GET; payloads with Date fields go through the /api route to match the client wire shape.
  • Purely additive: client components are unchanged; on a server-side prefetch failure the page still renders and the client refetches. Did NOT touch the workflow editor (socket-hydrated) or logs (URL-filter-dependent key).

Type of Change

  • Performance / improvement

Testing

  • prefetch.test.ts: 6 tests assert each prefetch primes the EXACT queryKey its client hook reads (imported from the real key factories, so hydration can't silently drift) + graceful-failure-no-throw + folder Date mapping.
  • Every prefetched key byte-verified against useFolders/useWorkspaceFiles/useWorkspaceFileFolders/useKnowledgeBasesList/useTablesList.
  • tsc 0 errors, check:react-query + check:api-validation pass, Biome clean.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

@vercel

vercel Bot commented Jun 24, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Jun 24, 2026 4:39pm

Request Review

@cursor

cursor Bot commented Jun 24, 2026

Copy link
Copy Markdown

PR Summary

Low Risk
Additive performance work with unchanged client components; main caveat is extra server-side internal API traffic per navigation and reliance on session cookies for those prefetches.

Overview
Adds server-side React Query prefetch on the workspace home, knowledge, tables, and files routes so list data is in the cache before the client hydrates, instead of loading only after mount.

Each page is now an async RSC that reads workspaceId, awaits route-specific prefetches (matching the same queryKeys and unwrap logic as useFolders, useWorkspaceFiles, etc.), then wraps the existing UI in HydrationBoundary with dehydrate(queryClient). Home still runs getSession() in parallel with list prefetch where possible; Suspense boundaries for nuqs/search-params pages are unchanged.

Shared prefetchInternalJson performs cookie-forwarded GETs to internal /api routes so serialized payloads (especially Date fields) match what the browser caches via requestJson. mapFolder and FOLDER_LIST_STALE_TIME are exported from folder queries so the home prefetch stores the same folder shape as the client hook. prefetch.test.ts locks query keys, response unwrapping, folder mapping, and prefetch failure behavior (no cache prime; client can refetch).

Reviewed by Cursor Bugbot for commit f0881cc. Configure here.

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile review

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 4b6eabb. Configure here.

@greptile-apps

greptile-apps Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR extends the existing server-prefetch pattern to four more workspace list pages (home, knowledge, tables, and files), eliminating the flash of empty content that previously occurred because those routes fetched data on mount rather than at the server render boundary.

  • A new shared prefetchInternalJson helper centralises the cookie-forwarding GET used by all four prefetches, and each prefetch function primes the exact React Query key its client hook reads — verified by the test suite importing real key factories.
  • HydrationBoundary wraps each page's client component so dehydrated data is available before React hydrates, and all prefetches are awaited before dehydrate() to guarantee the client always receives resolved (not pending) state.
  • The home page fires its prefetch concurrently with getSession() to avoid serial waiting, and folders.ts now exports mapFolder/FOLDER_LIST_STALE_TIME so the server-side path produces the exact same cached shape the client hook stores.

Confidence Score: 5/5

Safe to merge — the change is purely additive, server-prefetch failures are silently absorbed so no page is broken if a prefetch call fails, and client components are left untouched.

All four prefetch functions use the existing getQueryClient-per-request pattern, correctly await before dehydrate(), and match query keys and stale-times against their client hooks. The two observations are minor style inconsistencies with no user-visible impact.

No files require special attention.

Important Files Changed

Filename Overview
apps/sim/app/workspace/[workspaceId]/lib/prefetch-internal-fetch.ts New shared helper for server-side cookie-forwarding GET to internal API routes; correctly throws on non-200 so prefetchQuery catches failures gracefully, and uses headers() (dynamic API) which prevents Next.js from sharing the fetch cache across users.
apps/sim/app/workspace/[workspaceId]/lib/prefetch.test.ts Comprehensive test suite with parametrized graceful-failure coverage across all four prefetch functions; however the graceful-failure case for prefetchHomeLists only asserts the folder key is undefined, leaving the sibling workspaceFilesKeys.list entry unverified.
apps/sim/app/workspace/[workspaceId]/home/prefetch.ts Prefetches folders and workspace files under the exact keys useFolders/useWorkspaceFiles read; applies mapFolder for date hydration, uses ?? [] fallback on the unvalidated path, and re-uses the exported FOLDER_LIST_STALE_TIME constant to stay in sync with the client hook.
apps/sim/app/workspace/[workspaceId]/home/page.tsx Fires prefetchHomeLists concurrently with getSession() for maximum parallelism, then awaits both before dehydrate() so the client always hydrates with resolved data.
apps/sim/app/workspace/[workspaceId]/tables/prefetch.ts Prefetches tables list under the exact key useTablesList reads; response.data.tables is unguarded (consistent with the client hook but slightly more aggressive than sibling prefetches that use ?? []), though any TypeError is caught by prefetchQuery.
apps/sim/app/workspace/[workspaceId]/knowledge/prefetch.ts Prefetches knowledge bases list under the exact key useKnowledgeBasesQuery reads; returns result.data directly which mirrors the client fetchKnowledgeBases function exactly.
apps/sim/app/workspace/[workspaceId]/files/prefetch.ts Prefetches both workspace files and file folders under the exact keys useWorkspaceFiles/useWorkspaceFileFolders read; defensive ?? [] fallback on the folder path and success flag guard on files match the previous review feedback.
apps/sim/hooks/queries/folders.ts Exports mapFolder and FOLDER_LIST_STALE_TIME to allow the server-side home prefetch to produce the exact cached shape useFolders stores; purely additive, no logic changed.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Browser
    participant RSC as Server Component (Page)
    participant PF as prefetch*.ts
    participant API as Internal API Route
    participant QC as QueryClient
    participant HB as HydrationBoundary
    participant CC as Client Component

    Browser->>RSC: GET /workspace/[id]/home (etc.)
    RSC->>PF: prefetchHomeLists(queryClient, workspaceId)
    RSC->>RSC: getSession() [runs concurrently]
    PF->>API: GET /api/folders?... (cookie forwarded)
    PF->>API: GET /api/workspaces/.../files?... (cookie forwarded)
    API-->>PF: JSON responses
    PF->>QC: prefetchQuery → cache primed
    RSC->>RSC: dehydrate(queryClient)
    RSC->>HB: render with dehydrated state
    HB->>CC: hydrate with pre-filled cache
    CC-->>Browser: First paint with data (no loading flash)
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Browser
    participant RSC as Server Component (Page)
    participant PF as prefetch*.ts
    participant API as Internal API Route
    participant QC as QueryClient
    participant HB as HydrationBoundary
    participant CC as Client Component

    Browser->>RSC: GET /workspace/[id]/home (etc.)
    RSC->>PF: prefetchHomeLists(queryClient, workspaceId)
    RSC->>RSC: getSession() [runs concurrently]
    PF->>API: GET /api/folders?... (cookie forwarded)
    PF->>API: GET /api/workspaces/.../files?... (cookie forwarded)
    API-->>PF: JSON responses
    PF->>QC: prefetchQuery → cache primed
    RSC->>RSC: dehydrate(queryClient)
    RSC->>HB: render with dehydrated state
    HB->>CC: hydrate with pre-filled cache
    CC-->>Browser: First paint with data (no loading flash)
Loading

Reviews (3): Last reviewed commit: "perf(workspace): server-prefetch home, k..." | Re-trigger Greptile

Comment thread apps/sim/app/workspace/[workspaceId]/lib/prefetch-internal-fetch.ts
Comment thread apps/sim/app/workspace/[workspaceId]/files/prefetch.ts Outdated
@greptile-apps

greptile-apps Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR extends the existing server-side prefetch + HydrationBoundary pattern (already used for the workspace sidebar, settings, and subscription pages) to four more workspace list pages — home, knowledge, tables, and files — so those views paint with populated data on first render instead of waiting for client-side mount fetches.

  • A shared prefetchInternalJson helper centralises cookie-forwarding to internal /api routes, directly mirroring the inline pattern in the existing prefetchSubscriptionData function.
  • Each page's prefetchXxx function uses the exact same query-key factory as the corresponding client hook (verified in the 6-test suite that imports the real factories), so hydration alignment is mechanical rather than fragile string matching.
  • mapFolder and FOLDER_LIST_STALE_TIME are promoted to exports in folders.ts so the home-page server prefetch can produce the same WorkflowFolder[] shape (with Date objects) that useFolders caches, keeping the hydrated entry in sync with the client fetch shape through React RSC's native Date serialisation.

Confidence Score: 4/5

Safe to merge — the change is purely additive, existing client components are untouched, and pages fall back gracefully to a client refetch on any server prefetch failure.

The implementation is well-aligned with the established prefetch pattern in the codebase and query-key alignment is verified by tests importing real factory functions. The one non-trivial logic gap is in files/prefetch.ts: the file-folders queryFn returns data.folders without a null fallback, while the adjacent files queryFn defensively falls back to [] — an inconsistency that could leave undefined in the cache under unexpected API responses. The graceful-failure test suite also only exercises one of the four prefetch functions.

apps/sim/app/workspace/[workspaceId]/files/prefetch.ts — the folder queryFn missing null-fallback is the only concrete improvement needed; all other files look correct.

Important Files Changed

Filename Overview
apps/sim/app/workspace/[workspaceId]/lib/prefetch-internal-fetch.ts New shared helper that forwards cookie headers to internal /api routes; mirrors the existing subscription-prefetch pattern cleanly
apps/sim/app/workspace/[workspaceId]/lib/prefetch.test.ts Solid 6-test suite verifying exact query-key alignment and graceful failure; graceful-failure coverage limited to prefetchKnowledgeBases
apps/sim/app/workspace/[workspaceId]/home/prefetch.ts Prefetches folders (with mapFolder applied) and workspace files in parallel under the correct folderKeys/workspaceFilesKeys query keys
apps/sim/app/workspace/[workspaceId]/home/page.tsx Correctly parallelises prefetchHomeLists and getSession() before awaiting both; wraps in HydrationBoundary without breaking the existing Home session-prop contract
apps/sim/app/workspace/[workspaceId]/files/prefetch.ts Prefetches both workspace files (with success-fallback to []) and file-folders; folder query returns data.folders directly without a defensive empty-array fallback
apps/sim/app/workspace/[workspaceId]/knowledge/prefetch.ts Single prefetch under the correct knowledgeKeys.list key; unwraps result.data to match what useKnowledgeBasesQuery returns
apps/sim/app/workspace/[workspaceId]/tables/prefetch.ts Prefetches under tableKeys.list matching useTablesList; correctly navigates response.data.tables nesting to match the client hook return shape
apps/sim/app/workspace/[workspaceId]/knowledge/page.tsx Converts direct Knowledge export to async KnowledgePage wrapper with HydrationBoundary; consistent with client-component pattern used by Files and Tables
apps/sim/hooks/queries/folders.ts Exports mapFolder and FOLDER_LIST_STALE_TIME so the server prefetch produces the same cached shape as useFolders without duplicating mapping logic

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Browser
    participant NextRSC as Next.js RSC (Server)
    participant InternalAPI as Internal /api routes
    participant QueryClient as TanStack QueryClient
    participant HB as HydrationBoundary
    participant ClientHook as Client useXxx hooks

    Browser->>NextRSC: GET /workspace/[id]/home (or knowledge/tables/files)
    activate NextRSC
    NextRSC->>QueryClient: getQueryClient() — new per-request instance
    par Parallel prefetch
        NextRSC->>InternalAPI: "GET /api/folders?workspaceId=…&scope=active (cookie forwarded)"
        InternalAPI-->>NextRSC: "{ folders: [...] }"
        NextRSC->>QueryClient: prefetchQuery(folderKeys.list, mapFolder(rows))
    and
        NextRSC->>InternalAPI: "GET /api/workspaces/…/files?scope=active"
        InternalAPI-->>NextRSC: "{ success, files: [...] }"
        NextRSC->>QueryClient: prefetchQuery(workspaceFilesKeys.list, files)
    end
    NextRSC->>QueryClient: dehydrate(queryClient)
    NextRSC->>Browser: SSR HTML + dehydrated state prop to HydrationBoundary
    deactivate NextRSC
    Browser->>HB: React hydration — restore dehydrated queries into client QueryClient
    activate HB
    HB->>ClientHook: queries already cached and fresh within staleTime
    deactivate HB
    Note over ClientHook,Browser: First paint shows populated lists — no skeleton flash
    ClientHook-->>Browser: renders data immediately
    Note over ClientHook: After staleTime (30–60 s), hooks refetch normally
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Browser
    participant NextRSC as Next.js RSC (Server)
    participant InternalAPI as Internal /api routes
    participant QueryClient as TanStack QueryClient
    participant HB as HydrationBoundary
    participant ClientHook as Client useXxx hooks

    Browser->>NextRSC: GET /workspace/[id]/home (or knowledge/tables/files)
    activate NextRSC
    NextRSC->>QueryClient: getQueryClient() — new per-request instance
    par Parallel prefetch
        NextRSC->>InternalAPI: "GET /api/folders?workspaceId=…&scope=active (cookie forwarded)"
        InternalAPI-->>NextRSC: "{ folders: [...] }"
        NextRSC->>QueryClient: prefetchQuery(folderKeys.list, mapFolder(rows))
    and
        NextRSC->>InternalAPI: "GET /api/workspaces/…/files?scope=active"
        InternalAPI-->>NextRSC: "{ success, files: [...] }"
        NextRSC->>QueryClient: prefetchQuery(workspaceFilesKeys.list, files)
    end
    NextRSC->>QueryClient: dehydrate(queryClient)
    NextRSC->>Browser: SSR HTML + dehydrated state prop to HydrationBoundary
    deactivate NextRSC
    Browser->>HB: React hydration — restore dehydrated queries into client QueryClient
    activate HB
    HB->>ClientHook: queries already cached and fresh within staleTime
    deactivate HB
    Note over ClientHook,Browser: First paint shows populated lists — no skeleton flash
    ClientHook-->>Browser: renders data immediately
    Note over ClientHook: After staleTime (30–60 s), hooks refetch normally
Loading

Reviews (2): Last reviewed commit: "perf(workspace): server-prefetch home, k..." | Re-trigger Greptile

Comment thread apps/sim/app/workspace/[workspaceId]/files/prefetch.ts
Comment thread apps/sim/app/workspace/[workspaceId]/lib/prefetch.test.ts Outdated
@waleedlatif1 waleedlatif1 force-pushed the perf/workspace-list-prefetch branch from 4b6eabb to f0881cc Compare June 24, 2026 16:39
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile review

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit f0881cc. Configure here.

@waleedlatif1 waleedlatif1 merged commit 86dc04d into staging Jun 24, 2026
16 checks passed
@waleedlatif1 waleedlatif1 deleted the perf/workspace-list-prefetch branch June 24, 2026 17:45
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