mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 21:03:08 +00:00
fix: Prevent GitHub API rate limiting from frequent worktree PR fetching
Fixes #685 This commit addresses the GitHub API rate limit issue caused by excessive worktree PR status fetching. ## Changes ### Server-side PR caching (list.ts) - Added `GitHubPRCacheEntry` interface and `githubPRCache` Map - Implemented 2-minute TTL cache for GitHub PR data - Modified `fetchGitHubPRs()` to check cache before making API calls - Added `forceRefresh` parameter to bypass cache when explicitly requested - Cache is properly cleared when force refresh is triggered ### Frontend polling reduction (worktree-panel.tsx) - Increased worktree polling interval from 5 seconds to 30 seconds - Reduces polling frequency by 6x while keeping UI reasonably fresh - Updated comment to reflect new polling strategy ### Type improvements (use-worktrees.ts) - Fixed `fetchWorktrees` callback signature to accept `silent` option - Returns proper type for removed worktrees detection ## Impact - Combined ~12x reduction in GitHub API calls - 2-minute cache prevents repeated API hits during normal operation - 30-second polling balances responsiveness with API conservation - Force refresh option allows users to manually update when needed 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -39,8 +39,15 @@ interface GitHubRemoteCacheEntry {
|
|||||||
checkedAt: number;
|
checkedAt: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface GitHubPRCacheEntry {
|
||||||
|
prs: Map<string, WorktreePRInfo>;
|
||||||
|
fetchedAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
const githubRemoteCache = new Map<string, GitHubRemoteCacheEntry>();
|
const githubRemoteCache = new Map<string, GitHubRemoteCacheEntry>();
|
||||||
|
const githubPRCache = new Map<string, GitHubPRCacheEntry>();
|
||||||
const GITHUB_REMOTE_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
const GITHUB_REMOTE_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
||||||
|
const GITHUB_PR_CACHE_TTL_MS = 2 * 60 * 1000; // 2 minutes - avoid hitting GitHub on every poll
|
||||||
|
|
||||||
interface WorktreeInfo {
|
interface WorktreeInfo {
|
||||||
path: string;
|
path: string;
|
||||||
@@ -180,9 +187,24 @@ async function getGitHubRemoteStatus(projectPath: string): Promise<GitHubRemoteS
|
|||||||
* This also allows detecting PRs that were created outside the app.
|
* This also allows detecting PRs that were created outside the app.
|
||||||
*
|
*
|
||||||
* Uses cached GitHub remote status to avoid repeated warnings when the
|
* Uses cached GitHub remote status to avoid repeated warnings when the
|
||||||
* project doesn't have a GitHub remote configured.
|
* project doesn't have a GitHub remote configured. Results are cached
|
||||||
|
* briefly to avoid hammering GitHub on frequent worktree polls.
|
||||||
*/
|
*/
|
||||||
async function fetchGitHubPRs(projectPath: string): Promise<Map<string, WorktreePRInfo>> {
|
async function fetchGitHubPRs(
|
||||||
|
projectPath: string,
|
||||||
|
forceRefresh = false
|
||||||
|
): Promise<Map<string, WorktreePRInfo>> {
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
if (!forceRefresh) {
|
||||||
|
const cached = githubPRCache.get(projectPath);
|
||||||
|
if (cached && now - cached.fetchedAt < GITHUB_PR_CACHE_TTL_MS) {
|
||||||
|
return cached.prs;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
githubPRCache.delete(projectPath);
|
||||||
|
}
|
||||||
|
|
||||||
const prMap = new Map<string, WorktreePRInfo>();
|
const prMap = new Map<string, WorktreePRInfo>();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -225,6 +247,11 @@ async function fetchGitHubPRs(projectPath: string): Promise<Map<string, Worktree
|
|||||||
createdAt: pr.createdAt,
|
createdAt: pr.createdAt,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
githubPRCache.set(projectPath, {
|
||||||
|
prs: prMap,
|
||||||
|
fetchedAt: Date.now(),
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Silently fail - PR detection is optional
|
// Silently fail - PR detection is optional
|
||||||
logger.warn(`Failed to fetch GitHub PRs: ${getErrorMessage(error)}`);
|
logger.warn(`Failed to fetch GitHub PRs: ${getErrorMessage(error)}`);
|
||||||
@@ -364,7 +391,7 @@ export function createListHandler() {
|
|||||||
// Only fetch GitHub PRs if includeDetails is requested (performance optimization).
|
// Only fetch GitHub PRs if includeDetails is requested (performance optimization).
|
||||||
// Uses --state all to detect merged/closed PRs, limited to 1000 recent PRs.
|
// Uses --state all to detect merged/closed PRs, limited to 1000 recent PRs.
|
||||||
const githubPRs = includeDetails
|
const githubPRs = includeDetails
|
||||||
? await fetchGitHubPRs(projectPath)
|
? await fetchGitHubPRs(projectPath, forceRefreshGitHub)
|
||||||
: new Map<string, WorktreePRInfo>();
|
: new Map<string, WorktreePRInfo>();
|
||||||
|
|
||||||
for (const worktree of worktrees) {
|
for (const worktree of worktrees) {
|
||||||
|
|||||||
@@ -95,12 +95,20 @@ export function useWorktrees({
|
|||||||
);
|
);
|
||||||
|
|
||||||
// fetchWorktrees for backward compatibility - now just triggers a refetch
|
// fetchWorktrees for backward compatibility - now just triggers a refetch
|
||||||
const fetchWorktrees = useCallback(async () => {
|
// The silent option is accepted but not used (React Query handles loading states)
|
||||||
await queryClient.invalidateQueries({
|
// Returns removed worktrees array if any were detected, undefined otherwise
|
||||||
queryKey: queryKeys.worktrees.all(projectPath),
|
const fetchWorktrees = useCallback(
|
||||||
});
|
async (_options?: {
|
||||||
return refetch();
|
silent?: boolean;
|
||||||
}, [projectPath, queryClient, refetch]);
|
}): Promise<Array<{ path: string; branch: string }> | undefined> => {
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: queryKeys.worktrees.all(projectPath),
|
||||||
|
});
|
||||||
|
const result = await refetch();
|
||||||
|
return result.data?.removedWorktrees;
|
||||||
|
},
|
||||||
|
[projectPath, queryClient, refetch]
|
||||||
|
);
|
||||||
|
|
||||||
const currentWorktreePath = currentWorktree?.path ?? null;
|
const currentWorktreePath = currentWorktree?.path ?? null;
|
||||||
const selectedWorktree = currentWorktreePath
|
const selectedWorktree = currentWorktreePath
|
||||||
|
|||||||
@@ -383,13 +383,13 @@ export function WorktreePanel({
|
|||||||
|
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
// Periodic interval check (5 seconds) to detect branch changes on disk
|
// Periodic interval check (30 seconds) to detect branch changes on disk
|
||||||
// Reduced from 1s to 5s to minimize GPU/CPU usage from frequent re-renders
|
// Reduced polling to lessen repeated worktree list calls while keeping UI reasonably fresh
|
||||||
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
intervalRef.current = setInterval(() => {
|
intervalRef.current = setInterval(() => {
|
||||||
fetchWorktrees({ silent: true });
|
fetchWorktrees({ silent: true });
|
||||||
}, 5000);
|
}, 30000);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (intervalRef.current) {
|
if (intervalRef.current) {
|
||||||
|
|||||||
Reference in New Issue
Block a user