From f5332ad15022262ed5abec0a84465e9bdb86b9da Mon Sep 17 00:00:00 2001 From: shrimbly Date: Sat, 28 Mar 2026 20:00:35 +1300 Subject: [PATCH] perf: speed up workflow listing by reading only file headers Instead of parsing entire workflow JSON files (which can be many MB with embedded base64 data), read only the first 1KB to extract the name via regex. Drop nodeCount from the listing. Probe all directories in parallel. Co-Authored-By: Claude Opus 4.6 --- src/app/api/list-workflows/route.ts | 94 ++++++++++++------- .../quickstart/WorkflowBrowserView.tsx | 7 +- 2 files changed, 61 insertions(+), 40 deletions(-) diff --git a/src/app/api/list-workflows/route.ts b/src/app/api/list-workflows/route.ts index 0f19b9e6..d4084d96 100644 --- a/src/app/api/list-workflows/route.ts +++ b/src/app/api/list-workflows/route.ts @@ -6,10 +6,58 @@ import { validateWorkflowPath } from "@/utils/pathValidation"; interface WorkflowListEntry { name: string; directoryPath: string; - nodeCount: number; lastModified: number; } +// Read just enough of the file to find the "name" field without parsing the whole thing. +// Workflow JSON starts with { "version": 1, "name": "..." ... } so 1KB is plenty. +const HEAD_BYTES = 1024; + +async function probeWorkflow( + dirPath: string, + dirName: string +): Promise { + try { + const files = await fs.readdir(dirPath); + const jsonFiles = files.filter((f) => f.endsWith(".json")); + + for (const jsonFile of jsonFiles) { + const filePath = path.join(dirPath, jsonFile); + try { + const handle = await fs.open(filePath, "r"); + try { + const buf = Buffer.alloc(HEAD_BYTES); + const { bytesRead } = await handle.read(buf, 0, HEAD_BYTES, 0); + const head = buf.toString("utf-8", 0, bytesRead); + + // Quick check: must look like a workflow file + if (!head.includes('"version"') || !head.includes('"nodes"')) { + continue; + } + + // Extract name via regex — avoids JSON.parse on potentially huge files + const nameMatch = head.match(/"name"\s*:\s*"([^"]*)"/); + const name = nameMatch ? nameMatch[1] : dirName; + + const stat = await fs.stat(filePath); + return { + name, + directoryPath: dirPath, + lastModified: stat.mtimeMs, + }; + } finally { + await handle.close(); + } + } catch { + continue; + } + } + } catch { + // Can't read directory + } + return null; +} + export async function GET(request: NextRequest) { const parentPath = request.nextUrl.searchParams.get("path"); @@ -32,39 +80,16 @@ export async function GET(request: NextRequest) { const entries = await fs.readdir(parentPath, { withFileTypes: true }); const directories = entries.filter((e) => e.isDirectory()); - const workflows: WorkflowListEntry[] = []; - - for (const dir of directories) { - const dirPath = path.join(parentPath, dir.name); - try { - const files = await fs.readdir(dirPath); - const jsonFiles = files.filter((f) => f.endsWith(".json")); - - for (const jsonFile of jsonFiles) { - try { - const filePath = path.join(dirPath, jsonFile); - const content = await fs.readFile(filePath, "utf-8"); - const parsed = JSON.parse(content); + // Probe all directories in parallel + const results = await Promise.all( + directories.map((dir) => + probeWorkflow(path.join(parentPath, dir.name), dir.name) + ) + ); - if (parsed.version && parsed.nodes && parsed.edges) { - const stat = await fs.stat(filePath); - workflows.push({ - name: parsed.name || dir.name, - directoryPath: dirPath, - nodeCount: parsed.nodes.length, - lastModified: stat.mtimeMs, - }); - break; // Use first valid workflow file per directory - } - } catch { - continue; - } - } - } catch { - // Skip directories we can't read - continue; - } - } + const workflows = results.filter( + (r): r is WorkflowListEntry => r !== null + ); // Sort by most recently modified first workflows.sort((a, b) => b.lastModified - a.lastModified); @@ -74,7 +99,8 @@ export async function GET(request: NextRequest) { return NextResponse.json( { success: false, - error: error instanceof Error ? error.message : "Failed to list workflows", + error: + error instanceof Error ? error.message : "Failed to list workflows", }, { status: 500 } ); diff --git a/src/components/quickstart/WorkflowBrowserView.tsx b/src/components/quickstart/WorkflowBrowserView.tsx index ee78a98c..0e33b98a 100644 --- a/src/components/quickstart/WorkflowBrowserView.tsx +++ b/src/components/quickstart/WorkflowBrowserView.tsx @@ -11,7 +11,6 @@ import { interface WorkflowListEntry { name: string; directoryPath: string; - nodeCount: number; lastModified: number; } @@ -261,11 +260,7 @@ export function WorkflowBrowserView({ {entry.name} -
- - {entry.nodeCount} node{entry.nodeCount !== 1 ? "s" : ""} - - · +
{formatRelativeTime(entry.lastModified)}