Browse Source

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 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 4 months ago
parent
commit
f5332ad150
  1. 94
      src/app/api/list-workflows/route.ts
  2. 7
      src/components/quickstart/WorkflowBrowserView.tsx

94
src/app/api/list-workflows/route.ts

@ -6,10 +6,58 @@ import { validateWorkflowPath } from "@/utils/pathValidation";
interface WorkflowListEntry { interface WorkflowListEntry {
name: string; name: string;
directoryPath: string; directoryPath: string;
nodeCount: number;
lastModified: 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<WorkflowListEntry | null> {
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) { export async function GET(request: NextRequest) {
const parentPath = request.nextUrl.searchParams.get("path"); 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 entries = await fs.readdir(parentPath, { withFileTypes: true });
const directories = entries.filter((e) => e.isDirectory()); const directories = entries.filter((e) => e.isDirectory());
const workflows: WorkflowListEntry[] = []; // Probe all directories in parallel
const results = await Promise.all(
for (const dir of directories) { directories.map((dir) =>
const dirPath = path.join(parentPath, dir.name); probeWorkflow(path.join(parentPath, dir.name), 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);
if (parsed.version && parsed.nodes && parsed.edges) { const workflows = results.filter(
const stat = await fs.stat(filePath); (r): r is WorkflowListEntry => r !== null
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;
}
}
// Sort by most recently modified first // Sort by most recently modified first
workflows.sort((a, b) => b.lastModified - a.lastModified); workflows.sort((a, b) => b.lastModified - a.lastModified);
@ -74,7 +99,8 @@ export async function GET(request: NextRequest) {
return NextResponse.json( return NextResponse.json(
{ {
success: false, success: false,
error: error instanceof Error ? error.message : "Failed to list workflows", error:
error instanceof Error ? error.message : "Failed to list workflows",
}, },
{ status: 500 } { status: 500 }
); );

7
src/components/quickstart/WorkflowBrowserView.tsx

@ -11,7 +11,6 @@ import {
interface WorkflowListEntry { interface WorkflowListEntry {
name: string; name: string;
directoryPath: string; directoryPath: string;
nodeCount: number;
lastModified: number; lastModified: number;
} }
@ -261,11 +260,7 @@ export function WorkflowBrowserView({
<span className="text-sm font-medium text-neutral-200 group-hover:text-neutral-100 transition-colors"> <span className="text-sm font-medium text-neutral-200 group-hover:text-neutral-100 transition-colors">
{entry.name} {entry.name}
</span> </span>
<div className="flex items-center gap-2 mt-0.5"> <div className="mt-0.5">
<span className="text-xs text-neutral-500">
{entry.nodeCount} node{entry.nodeCount !== 1 ? "s" : ""}
</span>
<span className="text-neutral-700">·</span>
<span className="text-xs text-neutral-500"> <span className="text-xs text-neutral-500">
{formatRelativeTime(entry.lastModified)} {formatRelativeTime(entry.lastModified)}
</span> </span>

Loading…
Cancel
Save