feat: optimize diff loading performance with background pre-fetching

This commit is contained in:
Bohdan Triapitsyn
2025-12-17 16:16:28 +02:00
parent 34353e1970
commit 60dea43cc3
6 changed files with 346 additions and 146 deletions
+131 -5
View File
@@ -7,10 +7,10 @@ import type {
GitIdentitySummary,
} from '@/lib/api/types';
const GIT_POLL_BASE_INTERVAL = 10000;
const GIT_POLL_MAX_INTERVAL = 20000;
const GIT_POLL_BASE_INTERVAL = 5000;
const GIT_POLL_MAX_INTERVAL = 10000;
const GIT_POLL_BACKOFF_STEP = 5000;
const LOG_STALE_THRESHOLD = 30000;
const LOG_STALE_THRESHOLD = 10000;
interface DirectoryGitState {
isGitRepo: boolean | null;
@@ -51,6 +51,7 @@ interface GitStore {
getDiff: (directory: string, filePath: string) => { original: string; modified: string; fetchedAt: number } | null;
setDiff: (directory: string, filePath: string, diff: { original: string; modified: string }) => void;
clearDiffCache: (directory: string) => void;
fetchAllDiffs: (directory: string, git: GitAPI) => Promise<void>;
setLogMaxCount: (directory: string, maxCount: number) => void;
@@ -60,12 +61,19 @@ interface GitStore {
refresh: (git: GitAPI, options?: { force?: boolean }) => Promise<void>;
}
interface GitFileDiffResponse {
original: string;
modified: string;
path: string;
}
interface GitAPI {
checkIsGitRepository: (directory: string) => Promise<boolean>;
getGitStatus: (directory: string) => Promise<GitStatus>;
getGitBranches: (directory: string) => Promise<GitBranch>;
getGitLog: (directory: string, options?: { maxCount?: number }) => Promise<GitLogResponse>;
getCurrentGitIdentity: (directory: string) => Promise<GitIdentitySummary | null>;
getGitFileDiff: (directory: string, options: { path: string }) => Promise<GitFileDiffResponse>;
}
const createEmptyDirectoryState = (): DirectoryGitState => ({
@@ -132,6 +140,55 @@ const hasStatusChanged = (oldStatus: GitStatus | null, newStatus: GitStatus | nu
return false;
};
const getChangedFilePaths = (oldStatus: GitStatus | null, newStatus: GitStatus | null): Set<string> => {
const changed = new Set<string>();
if (!newStatus) return changed;
const oldFiles = oldStatus?.files ?? [];
const newFiles = newStatus.files ?? [];
const oldFileMap = new Map(oldFiles.map((f) => [f.path, f] as const));
const newFileMap = new Map(newFiles.map((f) => [f.path, f] as const));
const allFilePaths = new Set<string>([...oldFileMap.keys(), ...newFileMap.keys()]);
for (const filePath of allFilePaths) {
const oldFile = oldFileMap.get(filePath);
const newFile = newFileMap.get(filePath);
// Added/removed/renamed
if (!oldFile || !newFile) {
changed.add(filePath);
continue;
}
// Index/worktree state changed (indicates actual content/state changed)
if (oldFile.index !== newFile.index || oldFile.working_dir !== newFile.working_dir) {
changed.add(filePath);
continue;
}
}
const oldStats = oldStatus?.diffStats ?? {};
const newStats = newStatus.diffStats ?? {};
const allStatPaths = new Set<string>([...Object.keys(oldStats), ...Object.keys(newStats)]);
for (const filePath of allStatPaths) {
const oldEntry = oldStats[filePath];
const newEntry = newStats[filePath];
if (!oldEntry || !newEntry) {
changed.add(filePath);
continue;
}
if (oldEntry.insertions !== newEntry.insertions || oldEntry.deletions !== newEntry.deletions) {
changed.add(filePath);
}
}
return changed;
};
export const useGitStore = create<GitStore>()(
devtools(
(set, get) => ({
@@ -198,13 +255,34 @@ export const useGitStore = create<GitStore>()(
const newDirectories = new Map(get().directories);
const currentDirState = newDirectories.get(directory) ?? createEmptyDirectoryState();
const changedPaths = getChangedFilePaths(currentDirState.status, newStatus);
const oldPaths = new Set((currentDirState.status?.files ?? []).map((f) => f.path));
const newPaths = new Set((newStatus.files ?? []).map((f) => f.path));
const nextDiffCache = new Map(currentDirState.diffCache);
// Drop cache for removed files
for (const oldPath of oldPaths) {
if (!newPaths.has(oldPath)) {
nextDiffCache.delete(oldPath);
}
}
// Drop cache for files whose state/content changed
for (const filePath of changedPaths) {
nextDiffCache.delete(filePath);
}
const hasFileContentChange = changedPaths.size > 0;
newDirectories.set(directory, {
...currentDirState,
isGitRepo: true,
status: newStatus,
diffCache: new Map(),
diffCache: nextDiffCache,
lastStatusFetch: Date.now(),
lastStatusChange: Date.now(),
lastStatusChange: hasFileContentChange ? Date.now() : currentDirState.lastStatusChange,
});
set({ directories: newDirectories });
} else {
@@ -314,6 +392,9 @@ export const useGitStore = create<GitStore>()(
}
await get().fetchIdentity(directory, git);
// Pre-fetch all diffs so they're ready when user opens Diff tab
await get().fetchAllDiffs(directory, git);
},
getDiff: (directory, filePath) => {
@@ -339,6 +420,49 @@ export const useGitStore = create<GitStore>()(
}
},
fetchAllDiffs: async (directory, git) => {
const dirState = get().directories.get(directory);
if (!dirState?.status?.files || dirState.status.files.length === 0) return;
const files = dirState.status.files;
// Find files that need fetching (no cache)
const filesToFetch = files.filter((file) => !dirState.diffCache.has(file.path));
if (filesToFetch.length === 0) return;
// Fetch all diffs in parallel
const results = await Promise.allSettled(
filesToFetch.map(async (file) => {
const response = await git.getGitFileDiff(directory, { path: file.path });
return {
path: file.path,
diff: { original: response.original ?? '', modified: response.modified ?? '' }
};
})
);
// Update diff cache with results
const newDirectories = new Map(get().directories);
const currentDirState = newDirectories.get(directory);
if (!currentDirState) return;
const newDiffCache = new Map(currentDirState.diffCache);
const now = Date.now();
results.forEach((result) => {
if (result.status === 'fulfilled') {
newDiffCache.set(result.value.path, {
...result.value.diff,
fetchedAt: now
});
}
});
newDirectories.set(directory, { ...currentDirState, diffCache: newDiffCache });
set({ directories: newDirectories });
},
setLogMaxCount: (directory, maxCount) => {
const newDirectories = new Map(get().directories);
const dirState = newDirectories.get(directory) ?? createEmptyDirectoryState();
@@ -368,6 +492,8 @@ export const useGitStore = create<GitStore>()(
const statusChanged = await get().fetchStatus(activeDirectory, git, { silent: true });
if (statusChanged) {
await get().fetchLog(activeDirectory, git);
// Pre-fetch all diffs so they're ready when user opens Diff tab
await get().fetchAllDiffs(activeDirectory, git);
// Reset to base interval on changes
set({ currentPollInterval: GIT_POLL_BASE_INTERVAL });
} else {