diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 0000000000..574472e64a --- /dev/null +++ b/.jules/bolt.md @@ -0,0 +1,5 @@ +## 2026-04-23 - Optimizing Server-Side Data Fetching with React 'cache' + +**Learning:** React's 'cache' function is essential for deduplicating database queries and expensive computations (like JWT verification) within a single request lifecycle in Next.js. However, 'cache' is not available in the Edge Runtime (Middleware). + +**Action:** Use a conditional wrapper `(typeof cache === 'function' ? cache : (fn) => fn)` when memoizing functions that are shared between Server Components and Middleware to ensure compatibility while still gaining performance benefits where available. diff --git a/lib/auth/session.ts b/lib/auth/session.ts index 0c474c70aa..875b2c44cf 100644 --- a/lib/auth/session.ts +++ b/lib/auth/session.ts @@ -2,6 +2,7 @@ import { compare, hash } from 'bcryptjs'; import { SignJWT, jwtVerify } from 'jose'; import { cookies } from 'next/headers'; import { NewUser } from '@/lib/db/schema'; +import { cache } from 'react'; const key = new TextEncoder().encode(process.env.AUTH_SECRET); const SALT_ROUNDS = 10; @@ -30,12 +31,17 @@ export async function signToken(payload: SessionData) { .sign(key); } -export async function verifyToken(input: string) { +// ⚡ OPTIMIZATION: Memoize JWT verification to avoid redundant CPU-intensive decoding +// within a single request lifecycle. We use a conditional check for 'cache' to +// maintain compatibility with the Edge Runtime/Middleware. +export const verifyToken = (typeof cache === 'function' + ? cache + : (fn: any) => fn)(async (input: string) => { const { payload } = await jwtVerify(input, key, { algorithms: ['HS256'], }); return payload as SessionData; -} +}); export async function getSession() { const session = (await cookies()).get('session')?.value; diff --git a/lib/db/queries.ts b/lib/db/queries.ts index c607c8f746..243ad28a86 100644 --- a/lib/db/queries.ts +++ b/lib/db/queries.ts @@ -3,8 +3,14 @@ import { db } from './drizzle'; import { activityLogs, teamMembers, teams, users } from './schema'; import { cookies } from 'next/headers'; import { verifyToken } from '@/lib/auth/session'; +import { cache } from 'react'; -export async function getUser() { +// ⚡ OPTIMIZATION: Memoize user and team lookups to deduplicate database queries +// and JWT verifications within a single request lifecycle. We use a conditional +// check for 'cache' to maintain compatibility with the Edge Runtime/Middleware. +export const getUser = (typeof cache === 'function' + ? cache + : (fn: any) => fn)(async () => { const sessionCookie = (await cookies()).get('session'); if (!sessionCookie || !sessionCookie.value) { return null; @@ -34,7 +40,7 @@ export async function getUser() { } return user[0]; -} +}); export async function getTeamByStripeCustomerId(customerId: string) { const result = await db @@ -64,7 +70,9 @@ export async function updateTeamSubscription( .where(eq(teams.id, teamId)); } -export async function getUserWithTeam(userId: number) { +export const getUserWithTeam = (typeof cache === 'function' + ? cache + : (fn: any) => fn)(async (userId: number) => { const result = await db .select({ user: users, @@ -76,7 +84,7 @@ export async function getUserWithTeam(userId: number) { .limit(1); return result[0]; -} +}); export async function getActivityLogs() { const user = await getUser(); @@ -99,7 +107,9 @@ export async function getActivityLogs() { .limit(10); } -export async function getTeamForUser(userId: number) { +export const getTeamForUser = (typeof cache === 'function' + ? cache + : (fn: any) => fn)(async (userId: number) => { const result = await db.query.users.findFirst({ where: eq(users.id, userId), with: { @@ -126,4 +136,4 @@ export async function getTeamForUser(userId: number) { }); return result?.teamMembers[0]?.team || null; -} +});