Skip to content

fix(prefetch): drop the internal HTTP hop from every resource list prefetch - #6404

Closed
waleedlatif1 wants to merge 4 commits into
stagingfrom
fix/folder-file-load-waterfall
Closed

fix(prefetch): drop the internal HTTP hop from every resource list prefetch#6404
waleedlatif1 wants to merge 4 commits into
stagingfrom
fix/folder-file-load-waterfall

Conversation

@waleedlatif1

Copy link
Copy Markdown
Collaborator

Summary

  • On a hard refresh of /files the folders painted first and the files a beat later. Both are prefetched and hydrated together, so the files entry was never reaching the client: each prefetch reached its own API route over an internal HTTP request, prefetchQuery swallows a rejection, and shouldDehydrateQuery drops an errored entry. One failed request silently shipped a page with that list missing while its cheaper siblings hydrated fine — and nothing logged it. The files list is the heaviest of the pair (it joins share rows on top of file rows), so it lost that race first
  • Audited every resource prefetch — all five had the identical hole. files, home, tables, knowledge, and the shared pinned/members chrome now call the data layer directly, matching prefetchWorkspaceSidebar, whose TSDoc already documented this as the intended pattern. prefetch-internal-fetch.ts is deleted (zero callers left)
  • Per page load: 5 internal HTTP round-trips → 1 authz read + N direct reads. Each hop was re-running session + membership authz against the app's own public URL
  • Extracts listWorkspaceFilesWithShares, listPinnedItemsForViewer and listKnowledgeBasesForViewer so each route and its prefetch fill the same query key from one function and cannot drift. listPinnedItemsForViewer lifts a query out of a route handler's inline drizzle, so GET /api/pinned-items changes too
  • listKnowledgeBasesForViewer serializes dates: knowledgeBaseDataSchema types them z.string(), so reading the data layer directly would otherwise cache Date objects that violate the declared type and flip to strings on the first refetch. Tables deliberately needs no such mapper — TableDefinition declares Date | string, so both representations satisfy it
  • The routes each authorized their own read, so every prefetch now verifies membership once (instead of once per list) and caches nothing without access, leaving the client fetch to get the real 403
  • Also prefetches the viewer's pending invitations with the sidebar so the switcher's "View invitations" entry is present the frame the menu opens, and moves the invitation key factory into hooks/queries/utils/ so a server prefetch can hydrate it without pulling in the emcn toast surface. Renamed off my/mine onto the repo's …ForViewer convention

Type of Change

  • Bug fix

Testing

Tested manually.

  • Test suite rewritten against the data layer — the old one asserted the HTTP paths and would have silently rotted. New coverage the old suite lacked: a per-prefetch authorization test, a folder-tree-per-resourceType test, and a regression test that a failing files read no longer takes the sibling folder list down with it — the exact shape of the bug
  • Verified the tests can fail: removed the auth guard from files/prefetch.ts, watched the authorization test go red, restored it
  • bunx turbo run type-check · bun run lint · check:client-boundary · check:react-query · check:api-validation — all pass
  • Pre-existing, unrelated: lib/table/*.test.ts fail to collect in a worktree on a [postcss] tailwindcss error from packages/emcn/**/*.module.css. Confirmed identical on clean origin/staging with these changes stashed

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)

…efetch

On a hard refresh of /files the folders painted first and the files a beat
later. Both are prefetched and hydrated together, so the files entry was never
reaching the client: each prefetch reached its own API route over an internal
HTTP request, prefetchQuery swallows a rejection, and shouldDehydrateQuery
drops an errored entry. One failed request silently shipped a page with that
list missing while its cheaper siblings hydrated fine — and nothing logged it.
The files list is the heaviest of the pair, so it lost that race first.

Every resource prefetch — files, home, tables, knowledge, and the shared
pinned/members chrome — now calls the data layer directly, matching
prefetchWorkspaceSidebar. Per page load that turns 5 internal HTTP round-trips,
each re-running session and membership authz, into 1 authz read plus N direct
reads.

Extracts listWorkspaceFilesWithShares, listPinnedItemsForViewer and
listKnowledgeBasesForViewer so each route and its prefetch fill the same query
key from one function and cannot drift. listKnowledgeBasesForViewer serializes
dates because the contract types them z.string(): reading the data layer
directly would otherwise cache Date objects that violate the declared type and
flip to strings on the first refetch.

The routes each authorized their own read, so each prefetch now verifies
membership once and caches nothing without access, leaving the client fetch to
get the real 403.

Also prefetches the viewer's pending invitations with the sidebar so the
switcher's "View invitations" entry is present the frame the menu opens, and
moves the invitation key factory into hooks/queries/utils so a server prefetch
can hydrate it without importing the emcn toast surface.
@vercel

vercel Bot commented Aug 8, 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 Aug 8, 2026 5:07am

Request Review

@cursor

cursor Bot commented Aug 8, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Touches server-side data loading and authorization for multiple workspace surfaces; behavior is intended to mirror the API routes, but any mismatch in wire shapes or permission checks could affect first paint or hydration.

Overview
Fixes silent missing lists on hard refresh (e.g. files lagging behind folders) by stopping most server prefetches from looping through internal /api HTTP. Those hops could fail without surfacing; React Query then dropped the errored cache entry, so the client painted an empty list until a refetch.

Files, home, knowledge, pinned items, workspace files, and invitations now hydrate through shared …ForViewer query helpers—the same functions the API routes call—so dehydrated data matches requestJson and cannot drift. Prefetches check workspace permission once and skip caching when access is denied, so unauthorized users still get a real 403 from the client fetch.

Sidebar also prefetches pending invitations (with bounded join-preview concurrency) so “View invitations” appears when the switcher opens; invitation types/keys move to ViewerInvitation / invitationKeys.viewer() and a slim invitation-keys util for server imports.

Tables still use prefetchInternalJson (tool-registry boundary); that helper now logs failed prefetches. Tests cover auth gating, chrome keys, and isolated failure when one list read throws.

Reviewed by Cursor Bugbot for commit 7e37f2f. Configure here.

@greptile-apps

greptile-apps Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR replaces resource-list prefetches that called internal API routes with authorized direct data-layer reads while retaining the table route boundary.

  • Adds shared query functions for workspace files, pinned items, knowledge bases, and pending invitations.
  • Performs workspace authorization before direct server prefetches and hydrates pending invitations with the workspace sidebar.
  • Aligns server-prefetched values with the corresponding React Query keys and API data shapes.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
apps/sim/app/workspace/[workspaceId]/files/prefetch.ts Replaces files and folder HTTP prefetches with direct reads after the same effective workspace-permission check used by the files route.
apps/sim/app/workspace/[workspaceId]/knowledge/prefetch.ts Directly prefetches knowledge bases and folders after workspace authorization while preserving client query keys and mapped data shapes.
apps/sim/app/workspace/[workspaceId]/lib/prefetch-resource-list-chrome.ts Uses shared data-layer functions for viewer-scoped pins and workspace member profiles under the caller's authorization guard.
apps/sim/app/workspace/[workspaceId]/prefetch.ts Extends sidebar hydration to pending viewer invitations using the same key and array shape as the client query.
apps/sim/lib/invitations/pending.ts Centralizes invitation listing, preview generation, failure isolation, and wire-date serialization for route and prefetch reuse.
apps/sim/lib/knowledge/queries.ts Provides a shared viewer-aware knowledge-base query and serializes dates to match the declared API contract.
apps/sim/lib/pinned-items/queries.ts Centralizes viewer/workspace filtering, active-resource filtering, enum narrowing, and date serialization for pinned-item lists.
apps/sim/lib/workspace-files/queries.ts Centralizes workspace file/share assembly and validates the result through the API response schema.

Sequence Diagram

sequenceDiagram
  participant Page as Workspace page/layout
  participant Auth as Session and workspace authz
  participant Query as React Query cache
  participant Data as Shared data layer
  participant Browser as Browser hooks

  Page->>Auth: Resolve viewer and workspace access
  Auth-->>Page: Effective permission
  alt Viewer is authorized
    Page->>Query: Prefetch resource query
    Query->>Data: Direct list read
    Data-->>Query: Contract-aligned data
    Page-->>Browser: Dehydrated query state
    Browser->>Query: Read hydrated query key
  else Viewer is unauthorized
    Page-->>Browser: No resource data hydrated
    Browser->>Browser: Client request receives route authorization result
  end
Loading

Reviews (4): Last reviewed commit: "perf(invitations): bound the join-previe..." | Re-trigger Greptile

…eturns

GET /api/table does not return listTables rows: it drops metadata, runs every
column through normalizeColumn, serializes the three dates, and defaults the
job fields. The prefetch called listTables directly, so a hydrated entry held
un-normalized columns plus a field the client never sees, and swapped them out
on the first refetch.

Extracts listTablesForWorkspace so the route and the prefetch produce one
shape, matching what files, knowledge and pinned items already do here.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@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 1862a84. Configure here.

…re shape

check:tool-registry-boundary rejected the tables page: lib/table/service
transitively imports the executor, so reading listTables from a page.tsx pulls
the tool registry (~4,700 modules) into that route's graph. Breaking the chain
means relocating stripGroupDeps, latestJobsForTables and pendingDeleteMask out
of modules that import the executor — a lib/table refactor that wants its own
review rather than riding along here. The tables list therefore keeps going
through its route, which reshapes every row anyway, while its folders and list
chrome read the data layer.

prefetchInternalJson comes back for that one caller and now logs before it
throws: prefetchQuery swallows the rejection and shouldDehydrateQuery drops the
errored entry, which is exactly how the original bug stayed invisible.

Also parses listWorkspaceFilesWithShares through the route contract's response
schema. listWorkspaceFiles returns contentUpdatedAt, which the schema does not
declare and does not pass through, so the prefetch was caching a field a client
fetch never has and that vanished on the next refetch.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@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 eac836b. Configure here.

…ical path

listPendingInvitationsForViewer computed join previews in a serial loop, which
was affordable when the switcher dropdown was the only caller. The sidebar
prefetch now calls it on every workspace page render, and each preview issues
up to three sequential queries — so a viewer with pending invitations paid
roughly three round-trips per invitation before the first byte of every route
under the workspace layout.

Bounds the fan-out with mapWithConcurrency instead. The preview already
degrades to null on failure, which is what makes it safe under a mapper that
fails all-or-nothing, and a bound keeps the pooled-connection ceiling the
serial loop was protecting.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

Closing in favour of a minimal fix. This grew into a 28-file refactor of every resource prefetch, and it wasn't a strict improvement — the tables conversion had to be reverted for the tool-registry boundary, and prefetching invitations put join-preview queries on the render critical path. Reopening as two targeted fixes: the Files prefetch (the reported bug) and the invitation preview fan-out.

@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 7e37f2f. Configure here.

@waleedlatif1
waleedlatif1 deleted the fix/folder-file-load-waterfall branch August 8, 2026 17:50
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