mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-09 09:11:00 +08:00
fix(ui): Projects enrichment — pass include_details, cancel on unmount, cache schemas
The lazy-schema fix was ineffective: the UI never sent include_details, so list_projects returned names only, every card lacked totals, and the >1M-node skip compared against undefined — each Projects visit still ran a ~30 s kernel schema scan, and revisits stacked uncancelled sweeps that kept the store busy (the felt slowness). Now: list_projects(include_details:true) carries totals and indexed_at; the enrichment loop is generation-guarded (unmount or refresh abandons it), schemas are cached for the session, auto-fetch is capped at 20 projects of ≤200k nodes, and larger stores keep a card with totals and no chips. After a Projects visit the daemon log shows only sub-second requests. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { callTool } from "../api/rpc";
|
||||
import type { Project, SchemaInfo } from "../lib/types";
|
||||
|
||||
@@ -14,36 +14,63 @@ interface UseProjectsResult {
|
||||
refresh: () => void;
|
||||
}
|
||||
|
||||
/* Schemas above this size are never auto-fetched: get_graph_schema scans
|
||||
* every node and edge, which on a multi-million-node index takes ~30 s of
|
||||
* server time. The card still shows totals from list_projects; only the
|
||||
* per-label chips are absent. */
|
||||
const SCHEMA_LAZY_MAX_NODES = 200_000;
|
||||
/* And never enrich more than this many cards per visit. */
|
||||
const SCHEMA_LAZY_MAX_PROJECTS = 20;
|
||||
|
||||
/* Session-scoped schema cache: revisiting the Projects tab must not rescan
|
||||
* stores the server already walked for us once. */
|
||||
const schemaCache = new Map<string, SchemaInfo>();
|
||||
|
||||
export function useProjects(): UseProjectsResult {
|
||||
const [projects, setProjects] = useState<ProjectInfo[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
/* Schemas above this size are not fetched: get_graph_schema scans every
|
||||
* node and edge, which on a multi-million-node index takes ~30 s. The card
|
||||
* still shows totals from list_projects; only the label chips are absent. */
|
||||
const SCHEMA_LAZY_MAX_NODES = 1_000_000;
|
||||
/* Bumped on unmount/refresh so a stale enrichment loop stops issuing
|
||||
* requests and stops patching state. */
|
||||
const generationRef = useRef(0);
|
||||
|
||||
const fetchProjects = useCallback(async () => {
|
||||
const generation = ++generationRef.current;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await callTool<{ projects: Project[] }>("list_projects");
|
||||
/* include_details carries node/edge totals and indexed_at — without it
|
||||
* the tool returns names only and every count would need a schema scan. */
|
||||
const result = await callTool<{ projects: Project[] }>("list_projects", {
|
||||
include_details: true,
|
||||
limit: 100,
|
||||
});
|
||||
if (generation !== generationRef.current) return;
|
||||
const list = result.projects ?? [];
|
||||
|
||||
/* Render immediately from list_projects' own totals… */
|
||||
setProjects(list.map((p) => ({ project: p, schema: null })));
|
||||
setProjects(
|
||||
list.map((p) => ({ project: p, schema: schemaCache.get(p.name) ?? null })),
|
||||
);
|
||||
setLoading(false);
|
||||
|
||||
/* …then enrich with per-label schemas one at a time, patching each
|
||||
* card as its schema lands. Sequential on purpose: N parallel schema
|
||||
* scans would serialize behind the store lock anyway. */
|
||||
for (const p of list) {
|
||||
if ((p.nodes ?? 0) > SCHEMA_LAZY_MAX_NODES) continue;
|
||||
/* …then enrich small projects with per-label schemas, one at a time,
|
||||
* patching each card as its schema lands. Sequential on purpose — N
|
||||
* parallel scans would serialize behind the store lock anyway — and
|
||||
* abandoned as soon as the tab unmounts or refreshes. */
|
||||
const candidates = list
|
||||
.filter(
|
||||
(p) => !schemaCache.has(p.name) && (p.nodes ?? Infinity) <= SCHEMA_LAZY_MAX_NODES,
|
||||
)
|
||||
.slice(0, SCHEMA_LAZY_MAX_PROJECTS);
|
||||
for (const p of candidates) {
|
||||
if (generation !== generationRef.current) return;
|
||||
try {
|
||||
const schema = await callTool<SchemaInfo>("get_graph_schema", {
|
||||
project: p.name,
|
||||
});
|
||||
schemaCache.set(p.name, schema);
|
||||
if (generation !== generationRef.current) return;
|
||||
setProjects((prev) =>
|
||||
prev.map((info) =>
|
||||
info.project.name === p.name ? { ...info, schema } : info,
|
||||
@@ -54,6 +81,7 @@ export function useProjects(): UseProjectsResult {
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (generation !== generationRef.current) return;
|
||||
setError(e instanceof Error ? e.message : "Failed to fetch projects");
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -61,6 +89,10 @@ export function useProjects(): UseProjectsResult {
|
||||
|
||||
useEffect(() => {
|
||||
fetchProjects();
|
||||
return () => {
|
||||
/* Stop any in-flight enrichment loop when the tab goes away. */
|
||||
generationRef.current++;
|
||||
};
|
||||
}, [fetchProjects]);
|
||||
|
||||
return { projects, loading, error, refresh: fetchProjects };
|
||||
|
||||
Reference in New Issue
Block a user