From 06deac30a151c150ed3d8945de8bff557511748b Mon Sep 17 00:00:00 2001 From: Wojciech Nawrocki Date: Wed, 12 Aug 2026 14:24:12 -0400 Subject: [PATCH] fix: await io --- AGENTS.md | 2 ++ src/app/admin/components/AccessControl.tsx | 3 +++ src/app/admin/components/UserManagement.tsx | 6 +++++- src/app/layout.tsx | 14 +++++++------- src/lib/server/auth.ts | 2 ++ src/lib/server/config.ts | 4 +--- 6 files changed, 20 insertions(+), 11 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 57bede2e..7c5fef27 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -25,6 +25,8 @@ prefer `useServerAction` (`@/lib/client/util`) or (if `useServerAction` doesn't work) `useActionState` over manually storing response/error state with `useState`. - To call Server Functions on mount (e.g. to fetch data), use SWR. +- RSCs must call `await io()` before accessing server state (`getConfig`, `getAuth`, etc), + unless they have already used a request-time API (`cookies`, `params`, etc). # Error handling diff --git a/src/app/admin/components/AccessControl.tsx b/src/app/admin/components/AccessControl.tsx index 22cf6882..1af1080a 100644 --- a/src/app/admin/components/AccessControl.tsx +++ b/src/app/admin/components/AccessControl.tsx @@ -1,3 +1,5 @@ +import { io } from 'next/cache' + import { getConfig } from '@/lib/server/config' import { getDb } from '@/lib/server/db' @@ -5,6 +7,7 @@ import { AllowlistEditor } from './AllowlistEditor' import { RegistrationModeControl } from './RegistrationModeControl' export async function AccessControl() { + await io() const mode = getConfig().registrationMode const allowed = mode === 'restricted' ? await getDb().allowedGithubUser.findMany({ orderBy: { githubUsername: 'asc' } }) : [] diff --git a/src/app/admin/components/UserManagement.tsx b/src/app/admin/components/UserManagement.tsx index fd2b9724..9c537f46 100644 --- a/src/app/admin/components/UserManagement.tsx +++ b/src/app/admin/components/UserManagement.tsx @@ -1,11 +1,15 @@ +import { io } from 'next/cache' + import { requireAdmin } from '@/app/admin/actions' import { getDb } from '@/lib/server/db' import { UserRow } from './UserRow' export async function UserManagement() { + await io() + const db = getDb() const [users, session] = await Promise.all([ - getDb().user.findMany({ + db.user.findMany({ select: { id: true, name: true, isAdmin: true }, orderBy: { createdAt: 'asc' }, }), diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 0a6d63e1..c471546c 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,10 +1,10 @@ import '@/css/app.css' import type { Metadata } from 'next' +import { io } from 'next/cache' import { Open_Sans } from 'next/font/google' import Image from 'next/image' import Link from 'next/link' -import { connection } from 'next/server' import { type ReactNode, Suspense } from 'react' import { ConfigCtx } from '@/lib/contexts' @@ -30,7 +30,8 @@ export default async function RootLayout({ }: Readonly<{ children: ReactNode }>) { - // https://nextjs.org/docs/app/getting-started/caching#opting-out-of-the-static-shell + // FIXME: this is currently the only `Suspense` in the app, + // so we don't get static shells for any page. return ( {children} @@ -43,11 +44,10 @@ async function RootLayoutBody({ }: Readonly<{ children: ReactNode }>) { - // NOTE: together with the Suspense above, this makes the entire app dynamic: - // nothing is pre-rendered and every request recomputes its output HTML. - // This means we don't have to worry about cache invalidation, - // but will need fixing if it becomes a perf issue. - await connection() + // NOTE: Awaiting `io` makes the root layout dynamic + // (every request recomputes its output HTML), + // though static shells for pages may still be pre-rendered by `next build`. + await io() const serverCfg = getConfig() const clientCfg = { diff --git a/src/lib/server/auth.ts b/src/lib/server/auth.ts index 68a6dcee..e25dbc67 100644 --- a/src/lib/server/auth.ts +++ b/src/lib/server/auth.ts @@ -4,6 +4,7 @@ import { betterAuth, type SocialProviders } from 'better-auth' import { prismaAdapter } from 'better-auth/adapters/prisma' import { nextCookies } from 'better-auth/next-js' import crypto from 'crypto' +import { io } from 'next/cache' import { headers } from 'next/headers' import { unauthorized } from 'next/navigation' @@ -134,6 +135,7 @@ export async function getAuth(): Promise { /** Require an authenticated user. Throws `unauthorized()` if not logged in. */ export async function requireAuth(): Promise { + await io() const auth = await getAuth() const session = await auth.api.getSession({ headers: await headers() }) if (!session) unauthorized() diff --git a/src/lib/server/config.ts b/src/lib/server/config.ts index 95812926..3eb26cb8 100644 --- a/src/lib/server/config.ts +++ b/src/lib/server/config.ts @@ -164,9 +164,7 @@ function ensureConfigWatcher() { /** Return the server configuration. * - * The object may be mutated. `saveConfig()` must be called after any modifications. - * - * Use {@link CacheTag.serverConfig} in Server Components that read this. */ + * The object may be mutated. `saveConfig()` must be called after any modifications. */ export function getConfig(): ServerConfig { if (!g.__config) initConfig() return g.__config!