Merge pull request #238 from amal66/olp-pr/directory-fetch-storm

[Fix 01] Batch the directory modal's project fetch (N+1 getProject storm)
This commit is contained in:
Will Chen 2026-07-21 16:51:36 +08:00 committed by GitHub
commit eda088e143
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 58 additions and 17 deletions

View file

@ -152,9 +152,16 @@ async function attachChatCreatorLabels(
}
// GET /projects
// Pass ?include=documents to also receive each project's documents in the
// same response. The directory pickers (useDirectoryData) previously fanned
// out one GET /projects/:id per project to obtain those documents; with N
// projects that burst — auth check plus several DB queries per request —
// could overwhelm the Supabase gateway. Batching keeps it at one request
// and a fixed number of queries regardless of project count.
projectsRouter.get("/", requireAuth, async (req, res) => {
const userId = res.locals.userId as string;
const userEmail = res.locals.userEmail as string | undefined;
const includeDocuments = req.query.include === "documents";
const db = createServerSupabase();
const { data, error } = await db.rpc("get_projects_overview", {
@ -163,7 +170,45 @@ projectsRouter.get("/", requireAuth, async (req, res) => {
});
if (error) return void res.status(500).json({ detail: error.message });
res.json(data ?? []);
const projects = (data ?? []) as { id: string }[];
if (!includeDocuments || projects.length === 0) {
return void res.json(projects);
}
const { data: docs, error: docsError } = await db
.from("documents")
.select("*")
.in(
"project_id",
projects.map((p) => p.id),
)
.order("created_at", { ascending: true });
if (docsError)
return void res.status(500).json({ detail: docsError.message });
const docsTyped = (docs ?? []) as unknown as {
id: string;
project_id?: string | null;
user_id?: string | null;
current_version_id?: string | null;
}[];
await attachLatestVersionNumbers(db, docsTyped);
await attachActiveVersionPaths(db, docsTyped);
await attachDocumentOwnerLabels(db, docsTyped);
const docsByProject = new Map<string, typeof docsTyped>();
for (const doc of docsTyped) {
if (!doc.project_id) continue;
const bucket = docsByProject.get(doc.project_id);
if (bucket) bucket.push(doc);
else docsByProject.set(doc.project_id, [doc]);
}
res.json(
projects.map((p) => ({
...p,
documents: docsByProject.get(p.id) ?? [],
})),
);
});
// POST /projects

View file

@ -1,11 +1,7 @@
"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import {
getLibrary,
getProject,
listProjects,
} from "@/app/lib/mikeApi";
import { getLibrary, listProjects } from "@/app/lib/mikeApi";
import type { Document, LibraryFolder, Project } from "./types";
export type DirectoryTab = "files" | "templates" | "projects";
@ -45,17 +41,14 @@ async function loadTemplates() {
}
async function loadProjects() {
const projects = await listProjects();
const fullProjects = await Promise.all(
projects.map((project) => getProject(project.id)),
);
const projectCounts = new Map(
projects.map((project) => [project.id, project.document_count ?? 0]),
);
return fullProjects.map((project) => ({
// One batched request. Fanning out getProject(id) per project caused an
// N+1 burst on every directory-modal open that could overwhelm the
// Supabase gateway once an account had accumulated projects.
const projects = await listProjects({ includeDocuments: true });
return projects.map((project) => ({
...project,
document_count:
project.documents?.length ?? projectCounts.get(project.id) ?? 0,
project.documents?.length ?? project.document_count ?? 0,
}));
}

View file

@ -163,8 +163,11 @@ async function toApiError(response: Response, path: string) {
// Projects
// ---------------------------------------------------------------------------
export async function listProjects(): Promise<Project[]> {
return apiRequest<Project[]>("/projects");
export async function listProjects(options?: {
includeDocuments?: boolean;
}): Promise<Project[]> {
const query = options?.includeDocuments ? "?include=documents" : "";
return apiRequest<Project[]>(`/projects${query}`);
}
export async function createProject(