Description
π§© Background
fetchUserRepos is called from getFullDashboardData to power the developer dashboard. It uses a while loop to paginate through GitHub's REST API for a user's repositories.
π What's Wrong
The current pagination loop is:
let PAGE = 1;
const MAX_PAGES = 100;
while (PAGE <= MAX_PAGES) {
const res = await fetchWithRetry(
${GITHUB_REST_URL}/users/${username}/repos?per_page=100&page=${PAGE}&sort=pushed,
{ headers: getHeaders(), cache: 'no-store' }
);
const repos = (await res.json()) as GitHubRepo[];
allRepos.push(...repos);
if (repos.length < 100) break;
PAGE++;
}
MAX_PAGES = 100 with per_page = 100 means up to 10,000 sequential REST API calls for a single user. There are real GitHub accounts with 500β1,000+ repositories. For those users:
Each iteration awaits the previous one β this is sequential blocking, not parallel. A 500-repo user triggers 5 back-to-back requests before the page loads.
GitHub's authenticated REST API rate limit is 5,000 requests/hour shared across all REST + GraphQL calls. A single dashboard load for a prolific user can consume hundreds of those credits.
The full allRepos[] array is cached in TTLCache β for a 1,000-repo user this is a large in-memory object that can't be garbage-collected until the TTL expires.
π Files to Change
lib/github.ts β fetchUserRepos function
β
Proposed Fix
The developerScore formula only needs two things from repos: stargazers_count (for star count) and language (for language distribution). The top 100 repos sorted by recent push are sufficient for both. There is no need to paginate further.
Option A β Simplest (single page fetch):
typescriptexport async function fetchUserRepos(
username: string,
options: FetchOptions = {}
): Promise<GitHubRepo[]> {
if (!validateGitHubUsername(username)) {
throw new Error('Invalid GitHub username format');
}
const key = cacheKey('repos', username);
if (!options.bypassCache) {
const cached = reposCache.get(key);
if (cached) return cached;
}
// One page is sufficient β developerScore only needs stars + languages
const res = await fetchWithRetry(
${GITHUB_REST_URL}/users/${username}/repos?per_page=100&sort=pushed,
{ headers: getHeaders(), cache: 'no-store' }
);
if (!res.ok) throw new Error(GitHub REST API error: ${res.status});
const repos = (await res.json()) as GitHubRepo[];
if (!options.bypassCache) {
reposCache.set(key, repos, GITHUB_CACHE_TTL_MS);
}
return repos;
}
Option B β Capped pagination (if more repos are ever needed):
typescriptconst MAX_PAGES = 3; // 300 repos max β sufficient for score + language stats
π§ͺ Tests to Add
typescript// lib/github.test.ts
it('fetches at most one page of repos', async () => {
let fetchCallCount = 0;
vi.spyOn(global, 'fetch').mockImplementation(async (url) => {
if (String(url).includes('/repos')) fetchCallCount++;
return {
ok: true,
json: async () => Array(100).fill({ stargazers_count: 1, language: 'TypeScript' }),
} as Response;
});
await fetchUserRepos('some-prolific-user');
expect(fetchCallCount).toBeLessThanOrEqual(1);
});
it('does not throw for users with 0 repos', async () => {
vi.spyOn(global, 'fetch').mockResolvedValue({
ok: true,
json: async () => [],
} as Response);
await expect(fetchUserRepos('empty-user')).resolves.toEqual([]);
});
π¬ Commit Message
fix(github): cap repo pagination to prevent rate limit exhaustion
Steps to Reproduce
na
Expected Behavior
na
Screenshots / Logs
No response
GitHub Username (If applicable)
No response
Environment
Chrome
Description
π§© Background
fetchUserRepos is called from getFullDashboardData to power the developer dashboard. It uses a while loop to paginate through GitHub's REST API for a user's repositories.
π What's Wrong
The current pagination loop is:
let PAGE = 1;
const MAX_PAGES = 100;
while (PAGE <= MAX_PAGES) {
const res = await fetchWithRetry(
${GITHUB_REST_URL}/users/${username}/repos?per_page=100&page=${PAGE}&sort=pushed,{ headers: getHeaders(), cache: 'no-store' }
);
const repos = (await res.json()) as GitHubRepo[];
allRepos.push(...repos);
if (repos.length < 100) break;
PAGE++;
}
MAX_PAGES = 100 with per_page = 100 means up to 10,000 sequential REST API calls for a single user. There are real GitHub accounts with 500β1,000+ repositories. For those users:
Each iteration awaits the previous one β this is sequential blocking, not parallel. A 500-repo user triggers 5 back-to-back requests before the page loads.
GitHub's authenticated REST API rate limit is 5,000 requests/hour shared across all REST + GraphQL calls. A single dashboard load for a prolific user can consume hundreds of those credits.
The full allRepos[] array is cached in TTLCache β for a 1,000-repo user this is a large in-memory object that can't be garbage-collected until the TTL expires.
π Files to Change
lib/github.ts β fetchUserRepos function
β Proposed Fix
The developerScore formula only needs two things from repos: stargazers_count (for star count) and language (for language distribution). The top 100 repos sorted by recent push are sufficient for both. There is no need to paginate further.
Option A β Simplest (single page fetch):
typescriptexport async function fetchUserRepos(
username: string,
options: FetchOptions = {}
): Promise<GitHubRepo[]> {
if (!validateGitHubUsername(username)) {
throw new Error('Invalid GitHub username format');
}
const key = cacheKey('repos', username);
if (!options.bypassCache) {
const cached = reposCache.get(key);
if (cached) return cached;
}
// One page is sufficient β developerScore only needs stars + languages
const res = await fetchWithRetry(
${GITHUB_REST_URL}/users/${username}/repos?per_page=100&sort=pushed,{ headers: getHeaders(), cache: 'no-store' }
);
if (!res.ok) throw new Error(
GitHub REST API error: ${res.status});const repos = (await res.json()) as GitHubRepo[];
if (!options.bypassCache) {
reposCache.set(key, repos, GITHUB_CACHE_TTL_MS);
}
return repos;
}
Option B β Capped pagination (if more repos are ever needed):
typescriptconst MAX_PAGES = 3; // 300 repos max β sufficient for score + language stats
π§ͺ Tests to Add
typescript// lib/github.test.ts
it('fetches at most one page of repos', async () => {
let fetchCallCount = 0;
vi.spyOn(global, 'fetch').mockImplementation(async (url) => {
if (String(url).includes('/repos')) fetchCallCount++;
return {
ok: true,
json: async () => Array(100).fill({ stargazers_count: 1, language: 'TypeScript' }),
} as Response;
});
await fetchUserRepos('some-prolific-user');
expect(fetchCallCount).toBeLessThanOrEqual(1);
});
it('does not throw for users with 0 repos', async () => {
vi.spyOn(global, 'fetch').mockResolvedValue({
ok: true,
json: async () => [],
} as Response);
await expect(fetchUserRepos('empty-user')).resolves.toEqual([]);
});
π¬ Commit Message
fix(github): cap repo pagination to prevent rate limit exhaustion
Steps to Reproduce
na
Expected Behavior
na
Screenshots / Logs
No response
GitHub Username (If applicable)
No response
Environment
Chrome