diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 0000000..0440461 --- /dev/null +++ b/.jules/bolt.md @@ -0,0 +1,3 @@ +## 2026-03-30 - [Optimize LocalStore.listProjectFiles] +**Learning:** Sequential fs.stat calls in a for-loop create a significant bottleneck when listing large directories (e.g., projects with 1000+ files). Node.js can handle concurrent fs.stat operations much more efficiently. +**Action:** Use Promise.all with Array.prototype.map to perform fs.stat operations concurrently instead of sequentially for file listing operations. diff --git a/backend/src/store/localStore.ts b/backend/src/store/localStore.ts index 6aa075d..a890fe6 100644 --- a/backend/src/store/localStore.ts +++ b/backend/src/store/localStore.ts @@ -1,4 +1,4 @@ -import { readFile, stat, writeFile } from "node:fs/promises"; +import { readdir, readFile, stat, writeFile } from "node:fs/promises"; import { homedir } from "node:os"; import { dirname, join, relative } from "node:path"; import { URL } from "node:url"; @@ -114,17 +114,25 @@ export class LocalStore { } async listProjects(): Promise { - const entries = await listFilesRecursive(this.projectsDir); - const manifests: ProjectManifest[] = []; - for (const filePath of entries) { - if (!filePath.endsWith("manifest.json")) { - continue; - } - const manifest = await readJsonFile(filePath); - if (manifest) { - manifests.push(manifest); - } + let subdirs; + try { + subdirs = await readdir(this.projectsDir, { withFileTypes: true }); + } catch { + return []; } + + // Bolt: Optimized sequential file reading to concurrent Promise.all, + // and avoided O(N) listFilesRecursive for shallow manifest.json search. + const manifestPromises = subdirs + .filter(dirent => dirent.isDirectory()) + .map(async (dirent) => { + const manifestPath = join(this.projectsDir, dirent.name, "manifest.json"); + return readJsonFile(manifestPath); + }); + + const results = await Promise.all(manifestPromises); + const manifests = results.filter((m): m is ProjectManifest => m !== null); + manifests.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)); return manifests; } @@ -136,20 +144,23 @@ export class LocalStore { async listProjectFiles(projectId: string): Promise { const root = this.projectFilesDir(projectId); const files = await listFilesRecursive(root); - const out: ProjectFileEntry[] = []; - for (const absolutePath of files) { - try { - const stats = await stat(absolutePath); - const rel = relative(root, absolutePath).replace(/\\/g, "/"); - out.push({ - path: rel, - size: stats.size, - modifiedAt: stats.mtime.toISOString() - }); - } catch { - continue; - } - } + // Bolt: Optimized sequential stats to concurrent Promise.all + const entries = await Promise.all( + files.map(async (absolutePath) => { + try { + const stats = await stat(absolutePath); + const rel = relative(root, absolutePath).replace(/\\/g, "/"); + return { + path: rel, + size: stats.size, + modifiedAt: stats.mtime.toISOString() + }; + } catch { + return null; + } + }) + ); + const out = entries.filter((e): e is ProjectFileEntry => e !== null); out.sort((a, b) => a.path.localeCompare(b.path)); return out; }