fix: search performance

This commit is contained in:
Haileyesus
2026-04-27 22:23:50 +03:00
parent 9684aa0941
commit 5af2b719e2
4 changed files with 1082 additions and 816 deletions

View File

@@ -114,9 +114,9 @@ export function useSidebarController({
const [conversationResults, setConversationResults] = useState<ConversationSearchResults | null>(null);
const [isSearching, setIsSearching] = useState(false);
const [searchProgress, setSearchProgress] = useState<SearchProgress | null>(null);
const [debouncedSearchQuery, setDebouncedSearchQuery] = useState('');
const [optimisticStarByProjectId, setOptimisticStarByProjectId] = useState<Map<string, boolean>>(new Map());
const [loadingMoreProjects, setLoadingMoreProjects] = useState<Set<string>>(new Set());
const searchTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const searchSeqRef = useRef(0);
const eventSourceRef = useRef<EventSource | null>(null);
const starToggleSequenceByProjectRef = useRef<Map<string, number>>(new Map());
@@ -248,17 +248,26 @@ export function useSidebarController({
});
}, [projects]);
// Debounce search text updates so both project filtering and conversation
// SSE requests avoid running on every keypress.
useEffect(() => {
const timeout = setTimeout(() => {
setDebouncedSearchQuery(searchFilter.trim());
}, 300);
return () => {
clearTimeout(timeout);
};
}, [searchFilter]);
// Debounced conversation search with SSE streaming
useEffect(() => {
if (searchTimeoutRef.current) {
clearTimeout(searchTimeoutRef.current);
}
if (eventSourceRef.current) {
eventSourceRef.current.close();
eventSourceRef.current = null;
}
const query = searchFilter.trim();
const query = debouncedSearchQuery;
if (searchMode !== 'conversations' || query.length < 2) {
searchSeqRef.current += 1;
setConversationResults(null);
@@ -270,78 +279,75 @@ export function useSidebarController({
setIsSearching(true);
const seq = ++searchSeqRef.current;
searchTimeoutRef.current = setTimeout(() => {
if (seq !== searchSeqRef.current) return;
if (seq !== searchSeqRef.current) {
return;
}
const url = api.searchConversationsUrl(query);
const es = new EventSource(url);
eventSourceRef.current = es;
const url = api.searchConversationsUrl(query);
const es = new EventSource(url);
eventSourceRef.current = es;
const accumulated: ConversationProjectResult[] = [];
let totalMatches = 0;
const accumulated: ConversationProjectResult[] = [];
let totalMatches = 0;
es.addEventListener('result', (evt) => {
if (seq !== searchSeqRef.current) { es.close(); return; }
try {
const data = JSON.parse(evt.data) as {
projectResult: ConversationProjectResult;
totalMatches: number;
scannedProjects: number;
totalProjects: number;
};
accumulated.push(data.projectResult);
totalMatches = data.totalMatches;
setConversationResults({ results: [...accumulated], totalMatches, query });
setSearchProgress({ scannedProjects: data.scannedProjects, totalProjects: data.totalProjects });
} catch {
// Ignore malformed SSE data
}
});
es.addEventListener('result', (evt) => {
if (seq !== searchSeqRef.current) { es.close(); return; }
try {
const data = JSON.parse(evt.data) as {
projectResult: ConversationProjectResult;
totalMatches: number;
scannedProjects: number;
totalProjects: number;
};
accumulated.push(data.projectResult);
totalMatches = data.totalMatches;
setConversationResults({ results: [...accumulated], totalMatches, query });
setSearchProgress({ scannedProjects: data.scannedProjects, totalProjects: data.totalProjects });
} catch {
// Ignore malformed SSE data
}
});
es.addEventListener('progress', (evt) => {
if (seq !== searchSeqRef.current) { es.close(); return; }
try {
const data = JSON.parse(evt.data) as { totalMatches: number; scannedProjects: number; totalProjects: number };
totalMatches = data.totalMatches;
setSearchProgress({ scannedProjects: data.scannedProjects, totalProjects: data.totalProjects });
} catch {
// Ignore malformed SSE data
}
});
es.addEventListener('progress', (evt) => {
if (seq !== searchSeqRef.current) { es.close(); return; }
try {
const data = JSON.parse(evt.data) as { totalMatches: number; scannedProjects: number; totalProjects: number };
totalMatches = data.totalMatches;
setSearchProgress({ scannedProjects: data.scannedProjects, totalProjects: data.totalProjects });
} catch {
// Ignore malformed SSE data
}
});
es.addEventListener('done', () => {
if (seq !== searchSeqRef.current) { es.close(); return; }
es.close();
eventSourceRef.current = null;
setIsSearching(false);
setSearchProgress(null);
if (accumulated.length === 0) {
setConversationResults({ results: [], totalMatches: 0, query });
}
});
es.addEventListener('done', () => {
if (seq !== searchSeqRef.current) { es.close(); return; }
es.close();
eventSourceRef.current = null;
setIsSearching(false);
setSearchProgress(null);
if (accumulated.length === 0) {
setConversationResults({ results: [], totalMatches: 0, query });
}
});
es.addEventListener('error', () => {
if (seq !== searchSeqRef.current) { es.close(); return; }
es.close();
eventSourceRef.current = null;
setIsSearching(false);
setSearchProgress(null);
if (accumulated.length === 0) {
setConversationResults({ results: [], totalMatches: 0, query });
}
});
}, 400);
es.addEventListener('error', () => {
if (seq !== searchSeqRef.current) { es.close(); return; }
es.close();
eventSourceRef.current = null;
setIsSearching(false);
setSearchProgress(null);
if (accumulated.length === 0) {
setConversationResults({ results: [], totalMatches: 0, query });
}
});
return () => {
if (searchTimeoutRef.current) {
clearTimeout(searchTimeoutRef.current);
}
if (eventSourceRef.current) {
eventSourceRef.current.close();
eventSourceRef.current = null;
}
};
}, [searchFilter, searchMode]);
}, [debouncedSearchQuery, searchMode]);
// All sidebar state keys (expanded, starred, loading, etc.) use the DB
// `projectId` as their identifier after the migration.
@@ -493,8 +499,8 @@ export function useSidebarController({
);
const filteredProjects = useMemo(
() => filterProjects(sortedProjects, searchFilter),
[searchFilter, sortedProjects],
() => filterProjects(sortedProjects, debouncedSearchQuery),
[debouncedSearchQuery, sortedProjects],
);
const startEditing = useCallback((project: Project) => {