refactor: improve session limit and offset validation in provider routes

This commit is contained in:
Haileyesus
2026-04-29 11:46:53 +03:00
parent 10f35c238d
commit 0f93ef2781
4 changed files with 105 additions and 48 deletions

View File

@@ -338,21 +338,28 @@ router.get(
const limitRaw = readOptionalQueryString(req.query.limit); const limitRaw = readOptionalQueryString(req.query.limit);
const offsetRaw = readOptionalQueryString(req.query.offset); const offsetRaw = readOptionalQueryString(req.query.offset);
const limit = limitRaw === undefined ? null : Number.parseInt(limitRaw, 10); let limit: number | null = null;
const offset = offsetRaw === undefined ? 0 : Number.parseInt(offsetRaw, 10); if (limitRaw !== undefined) {
const parsedLimit = Number.parseInt(limitRaw, 10);
if (limitRaw !== undefined && Number.isNaN(limit)) { if (Number.isNaN(parsedLimit) || parsedLimit < 0) {
throw new AppError('limit must be a valid integer.', { throw new AppError('limit must be a non-negative integer.', {
code: 'INVALID_QUERY_PARAMETER', code: 'INVALID_QUERY_PARAMETER',
statusCode: 400, statusCode: 400,
}); });
}
limit = parsedLimit;
} }
if (offsetRaw !== undefined && Number.isNaN(offset)) { let offset = 0;
throw new AppError('offset must be a valid integer.', { if (offsetRaw !== undefined) {
code: 'INVALID_QUERY_PARAMETER', const parsedOffset = Number.parseInt(offsetRaw, 10);
statusCode: 400, if (Number.isNaN(parsedOffset) || parsedOffset < 0) {
}); throw new AppError('offset must be a non-negative integer.', {
code: 'INVALID_QUERY_PARAMETER',
statusCode: 400,
});
}
offset = parsedOffset;
} }
const result = await sessionsService.fetchHistory(sessionId, { const result = await sessionsService.fetchHistory(sessionId, {

View File

@@ -196,10 +196,14 @@ function createWordMatcher(
return phraseRegex.test(text); return phraseRegex.test(text);
} }
if (phraseRegex.test(text) || words.length === 1) { if (phraseRegex.test(text)) {
return true; return true;
} }
if (words.length === 1) {
return allWordsMatch(text.toLowerCase());
}
return allWordsMatch(text.toLowerCase()); return allWordsMatch(text.toLowerCase());
}; };
@@ -534,31 +538,47 @@ async function findMatchedFileKeys(
const requireExactPhrase = words.length > 1 && normalizedQuery.length > 0; const requireExactPhrase = words.length > 1 && normalizedQuery.length > 0;
if (requireExactPhrase) { if (requireExactPhrase) {
const matchedForPhrase = new Set<string>(); let matchedForPhrase = searchablePathEntries.slice();
const fileChunks = chunkArray(
searchablePathEntries.map((entry) => entry.absolutePath),
RIPGREP_FILE_CHUNK_SIZE,
);
let nextChunkIndex = 0; // Keep ripgrep as an over-approximation for exact phrase mode by requiring
const workerCount = Math.min(RIPGREP_CHUNK_CONCURRENCY, fileChunks.length); // each word to appear somewhere in the file, then defer strict phrase
const workers = Array.from({ length: workerCount }, async () => { // validation to the in-memory matcher.
while (nextChunkIndex < fileChunks.length && !signal?.aborted) { for (const word of words) {
const currentIndex = nextChunkIndex; if (signal?.aborted) {
nextChunkIndex += 1; return new Set();
const chunkMatches = await runRipgrepFilesWithMatches(normalizedQuery, fileChunks[currentIndex], signal);
for (const matchedPath of chunkMatches) {
matchedForPhrase.add(matchedPath);
}
} }
});
await Promise.all(workers); const matchedForWord = new Set<string>();
if (signal?.aborted) { const fileChunks = chunkArray(
return new Set(); matchedForPhrase.map((entry) => entry.absolutePath),
RIPGREP_FILE_CHUNK_SIZE,
);
let nextChunkIndex = 0;
const workerCount = Math.min(RIPGREP_CHUNK_CONCURRENCY, fileChunks.length);
const workers = Array.from({ length: workerCount }, async () => {
while (nextChunkIndex < fileChunks.length && !signal?.aborted) {
const currentIndex = nextChunkIndex;
nextChunkIndex += 1;
const chunkMatches = await runRipgrepFilesWithMatches(word, fileChunks[currentIndex], signal);
for (const matchedPath of chunkMatches) {
matchedForWord.add(matchedPath);
}
}
});
await Promise.all(workers);
if (signal?.aborted) {
return new Set();
}
matchedForPhrase = matchedForPhrase.filter((entry) => matchedForWord.has(entry.normalizedPath));
if (matchedForPhrase.length === 0) {
break;
}
} }
return matchedForPhrase; return new Set(matchedForPhrase.map((entry) => entry.normalizedPath));
} }
let remainingEntries = searchablePathEntries.slice(); let remainingEntries = searchablePathEntries.slice();

View File

@@ -120,6 +120,8 @@ export function useSidebarController({
const searchSeqRef = useRef(0); const searchSeqRef = useRef(0);
const eventSourceRef = useRef<EventSource | null>(null); const eventSourceRef = useRef<EventSource | null>(null);
const starToggleSequenceByProjectRef = useRef<Map<string, number>>(new Map()); const starToggleSequenceByProjectRef = useRef<Map<string, number>>(new Map());
const migrationStartedRef = useRef(false);
const onRefreshRef = useRef(onRefresh);
const isSidebarCollapsed = !isMobile && !sidebarVisible; const isSidebarCollapsed = !isMobile && !sidebarVisible;
@@ -194,19 +196,25 @@ export function useSidebarController({
}, []); }, []);
useEffect(() => { useEffect(() => {
onRefreshRef.current = onRefresh;
}, [onRefresh]);
useEffect(() => {
if (migrationStartedRef.current) {
return;
}
const legacyStarredProjectIds = readLegacyStarredProjectIds(); const legacyStarredProjectIds = readLegacyStarredProjectIds();
if (legacyStarredProjectIds.length === 0) { if (legacyStarredProjectIds.length === 0) {
return; return;
} }
let active = true; migrationStartedRef.current = true;
const migrateLegacyStars = async () => { const migrateLegacyStars = async () => {
try { try {
await api.migrateLegacyProjectStars(legacyStarredProjectIds); await api.migrateLegacyProjectStars(legacyStarredProjectIds);
if (active) { await onRefreshRef.current();
await onRefresh();
}
} catch (error) { } catch (error) {
console.error('[Sidebar] Failed to migrate legacy starred projects:', error); console.error('[Sidebar] Failed to migrate legacy starred projects:', error);
} finally { } finally {
@@ -215,10 +223,6 @@ export function useSidebarController({
}; };
void migrateLegacyStars(); void migrateLegacyStars();
return () => {
active = false;
};
}, [onRefresh]); }, [onRefresh]);
useEffect(() => { useEffect(() => {
@@ -446,16 +450,26 @@ export function useSidebarController({
const getProjectSessions = useCallback((project: Project) => getAllSessions(project), []); const getProjectSessions = useCallback((project: Project) => getAllSessions(project), []);
const loadMoreSessionsForProject = useCallback(async (projectId: string) => { const loadMoreSessionsForProject = useCallback(async (projectId: string) => {
if (!onLoadMoreSessions || loadingMoreProjects.has(projectId)) { if (!onLoadMoreSessions) {
return; return;
} }
let shouldLoad = false;
setLoadingMoreProjects((previous) => { setLoadingMoreProjects((previous) => {
if (previous.has(projectId)) {
return previous;
}
shouldLoad = true;
const next = new Set(previous); const next = new Set(previous);
next.add(projectId); next.add(projectId);
return next; return next;
}); });
if (!shouldLoad) {
return;
}
try { try {
await onLoadMoreSessions(projectId); await onLoadMoreSessions(projectId);
} catch (error) { } catch (error) {
@@ -468,7 +482,7 @@ export function useSidebarController({
return next; return next;
}); });
} }
}, [loadingMoreProjects, onLoadMoreSessions, t]); }, [onLoadMoreSessions, t]);
const projectsWithResolvedStarState = useMemo(() => { const projectsWithResolvedStarState = useMemo(() => {
if (optimisticStarByProjectId.size === 0) { if (optimisticStarByProjectId.size === 0) {

View File

@@ -605,12 +605,28 @@ export function useProjectsState({
setProjects((prevProjects) => setProjects((prevProjects) =>
prevProjects.map((project) => { prevProjects.map((project) => {
const sessions = project.sessions?.filter((session) => session.id !== sessionIdToDelete) ?? [];
const cursorSessions = project.cursorSessions?.filter((session) => session.id !== sessionIdToDelete) ?? [];
const codexSessions = project.codexSessions?.filter((session) => session.id !== sessionIdToDelete) ?? [];
const geminiSessions = project.geminiSessions?.filter((session) => session.id !== sessionIdToDelete) ?? [];
const removedFromProject = (
sessions.length !== (project.sessions?.length ?? 0)
|| cursorSessions.length !== (project.cursorSessions?.length ?? 0)
|| codexSessions.length !== (project.codexSessions?.length ?? 0)
|| geminiSessions.length !== (project.geminiSessions?.length ?? 0)
);
if (!removedFromProject) {
return project;
}
const updatedProject: Project = { const updatedProject: Project = {
...project, ...project,
sessions: project.sessions?.filter((session) => session.id !== sessionIdToDelete) ?? [], sessions,
cursorSessions: project.cursorSessions?.filter((session) => session.id !== sessionIdToDelete) ?? [], cursorSessions,
codexSessions: project.codexSessions?.filter((session) => session.id !== sessionIdToDelete) ?? [], codexSessions,
geminiSessions: project.geminiSessions?.filter((session) => session.id !== sessionIdToDelete) ?? [], geminiSessions,
}; };
const totalSessions = Math.max(0, Number(project.sessionMeta?.total ?? 0) - 1); const totalSessions = Math.max(0, Number(project.sessionMeta?.total ?? 0) - 1);