Commit28b52be
perf(repo-home): stop re-fetching the same owner/repo rows on every load
perf(repo-home): stop re-fetching the same owner/repo rows on every load
The repo home page issued ~9 sequential round-trips to Neon, and four of
them re-read rows it already had:
assertRepoReadable users <- lookup 1
assertRepoReadable repositories <- lookup 2
resolveRepoAccess repositories
starInfo users <- same row as lookup 1
starInfo repositories <- same row as lookup 2
starInfo stars
countPendingForRepo ...
onboarding repositories <- same row as lookup 2, again
onboarding repoOnboardingData
Every one of those is a serial network hop to managed Postgres, which is
why an owner-authenticated repo home measured 7.8s against 2.1s for the
same page anonymous — the gap scaled with page complexity, not with auth.
- Collapse assertRepoReadable's users + repositories lookups into a single
innerJoin, and select the full repo row rather than three columns (same
round trip, strictly more data).
- Publish the resolved {ownerRow, repoRow, access} on the request context
as `resolvedRepoCtx` (declared on AuthEnv), so downstream blocks reuse
what the access gate already fetched.
- starInfo and the onboarding gate now read that instead of re-querying.
Net for a populated repo: 9 sequential lookups -> 4. The signature of
assertRepoReadable is unchanged, so all 12 call sites are untouched and
the other 11 pick up the join for free.
Typecheck clean. Test suite: 3122 pass / 4 fail — the same 4 that already
fail on main, unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>2 files changed+41−5028b52be310269a33b991ec6a44f0a6d7ce0e59f7
2 changed files+41−50
Modifiedsrc/middleware/auth.ts+11−1View fileUnifiedSplit
@@ -12,7 +12,7 @@ import { getCookie } from "hono/cookie";
1212import { eq, gt } from "drizzle-orm";
1313import { db } from "../db";
1414import { sessions, users, oauthAccessTokens, apiTokens } from "../db/schema";
15import type { User } from "../db/schema";
15import type { User, Repository } from "../db/schema";
1616import { sessionCache } from "../lib/cache";
1717import { sha256Hex } from "../lib/oauth";
1818
@@ -30,6 +30,16 @@ export type AuthEnv = {
3030 requestStart?: number;
3131 /** Set by `requireRepoAccess` middleware — the resolved repository row. */
3232 repository?: unknown;
33 /**
34 * Set by web.tsx's `assertRepoReadable` gate — the owner + repository rows
35 * it already fetched (and access-checked) for this request, so downstream
36 * handlers can reuse them instead of re-issuing the same lookups.
37 */
38 resolvedRepoCtx?: {
39 ownerRow: User;
40 repoRow: Repository;
41 access: string;
42 };
3343 /** Set by `requireRepoAccess` — the caller's access level ('read'|'write'|'admin'|'owner'). */
3444 repoAccess?: string;
3545 /** Set by API-auth middleware — how the caller authenticated. */
Modifiedsrc/routes/web.tsx+30−49View fileUnifiedSplit
@@ -136,28 +136,25 @@ async function assertRepoReadable(
136136 // Case-insensitive slug match; carry back the CANONICAL owner/repo so we
137137 // can (a) never feed the caller's raw casing to the git layer and (b)
138138 // redirect to the canonical URL below.
139 const [ownerRow] = await db
140 .select({ id: users.id, username: users.username })
141 .from(users)
142 .where(sql`lower(${users.username}) = lower(${owner})`)
143 .limit(1);
144 if (!ownerRow) return notFound();
145
146 const [repoRow] = await db
147 .select({
148 id: repositories.id,
149 name: repositories.name,
150 isPrivate: repositories.isPrivate,
151 })
139 // Single joined lookup. This used to be two sequential round-trips
140 // (users, then repositories); on a managed Postgres every hop is real
141 // latency and the repo-home handler re-fetched these same two rows again
142 // further down. Fetch the FULL repo row once and publish it on the
143 // context (below) so downstream blocks can reuse it instead of re-querying.
144 const [joined] = await db
145 .select({ owner: users, repo: repositories })
152146 .from(repositories)
147 .innerJoin(users, eq(repositories.ownerId, users.id))
153148 .where(
154149 and(
155 eq(repositories.ownerId, ownerRow.id),
150 sql`lower(${users.username}) = lower(${owner})`,
156151 sql`lower(${repositories.name}) = lower(${repo})`
157152 )
158153 )
159154 .limit(1);
160 if (!repoRow) return notFound();
155 if (!joined) return notFound();
156 const ownerRow = joined.owner;
157 const repoRow = joined.repo;
161158
162159 const access = await resolveRepoAccess({
163160 repoId: repoRow.id,
@@ -183,6 +180,12 @@ async function assertRepoReadable(
183180 return c.redirect(segs.join("/") + search, 302);
184181 }
185182
183 // Publish the already-resolved rows for this request. Handlers that need
184 // the owner/repo row can read this instead of issuing their own lookups —
185 // repo-home alone was re-fetching `users` once and `repositories` twice
186 // more, all sequentially, for rows we already had in hand.
187 c.set("resolvedRepoCtx", { ownerRow, repoRow, access });
188
186189 return null;
187190 } catch (err) {
188191 // Fail CLOSED. A DB is configured (checked above) but the privacy-flag
@@ -2994,34 +2997,12 @@ web.get("/:owner/:repo", async (c) => {
29942997 // Star info fetched in parallel with tree
29952998 (async () => {
29962999 try {
2997 const [ownerUser] = await db
2998 .select()
2999 .from(users)
3000 .where(eq(users.username, owner))
3001 .limit(1);
3002 if (!ownerUser)
3003 return {
3004 starCount: 0,
3005 starred: false,
3006 archived: false,
3007 isTemplate: false,
3008 forkCount: 0,
3009 description: null as string | null,
3010 pushedAt: null as Date | null,
3011 createdAt: null as Date | null,
3012 repoId: null as string | null,
3013 repoOwnerId: null as string | null,
3014 };
3015 const [repoRow] = await db
3016 .select()
3017 .from(repositories)
3018 .where(
3019 and(
3020 eq(repositories.ownerId, ownerUser.id),
3021 eq(repositories.name, repo)
3022 )
3023 )
3024 .limit(1);
3000 // assertRepoReadable already resolved (and access-checked) both rows
3001 // for this request — reuse them instead of repeating the two lookups.
3002 const resolved = c.get("resolvedRepoCtx") as
3003 | { repoRow: typeof repositories.$inferSelect }
3004 | undefined;
3005 const repoRow = resolved?.repoRow;
30253006 if (!repoRow)
30263007 return {
30273008 starCount: 0,
@@ -3148,12 +3129,12 @@ web.get("/:owner/:repo", async (c) => {
31483129 const repoLooksEmpty = tree.length === 0 && branches.length === 0;
31493130 if (repoLooksEmpty && repoId && user && repoOwnerId && user.id === repoOwnerId) {
31503131 try {
3151 // Check if onboarding_shown is still false (not yet dismissed)
3152 const [repoRow2] = await db
3153 .select({ onboardingShown: repositories.onboardingShown })
3154 .from(repositories)
3155 .where(eq(repositories.id, repoId))
3156 .limit(1);
3132 // onboarding_shown comes from the row assertRepoReadable already
3133 // resolved — no need for a third lookup of the same repository.
3134 const resolvedForOnboarding = c.get("resolvedRepoCtx") as
3135 | { repoRow: typeof repositories.$inferSelect }
3136 | undefined;
3137 const repoRow2 = resolvedForOnboarding?.repoRow;
31573138 if (repoRow2 && !repoRow2.onboardingShown) {
31583139 const [obRow] = await db
31593140 .select()
31603141