Skip to content

Commit 52a120a

Browse files
committed
feat: implement throttling for GitHub API requests to optimize avatar fetching
1 parent 20a18ea commit 52a120a

File tree

1 file changed

+38
-2
lines changed

1 file changed

+38
-2
lines changed

src/lib/formatters.ts

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,39 @@ const HARDCODED_BOTS = new Set(botAvatarCache.keys());
288288
/** In-flight fetch promises to avoid duplicate API calls */
289289
const pendingFetches = new Map<string, Promise<string | null>>();
290290

291+
/** Simple throttle: max concurrent GitHub API requests */
292+
const MAX_CONCURRENT_FETCHES = 3;
293+
let activeFetchCount = 0;
294+
const fetchQueue: Array<() => void> = [];
295+
296+
function runNextFetch(): void {
297+
if (fetchQueue.length > 0 && activeFetchCount < MAX_CONCURRENT_FETCHES) {
298+
activeFetchCount++;
299+
const next = fetchQueue.shift()!;
300+
next();
301+
}
302+
}
303+
304+
function throttledFetch(url: string): Promise<Response> {
305+
return new Promise((resolve, reject) => {
306+
const execute = () => {
307+
fetch(url)
308+
.then(resolve, reject)
309+
.finally(() => {
310+
activeFetchCount--;
311+
runNextFetch();
312+
});
313+
};
314+
315+
if (activeFetchCount < MAX_CONCURRENT_FETCHES) {
316+
activeFetchCount++;
317+
execute();
318+
} else {
319+
fetchQueue.push(execute);
320+
}
321+
});
322+
}
323+
291324
/**
292325
* Resolve a bot's avatar URL from the GitHub API and cache it.
293326
* Returns the avatar_url or null if the lookup fails.
@@ -300,7 +333,7 @@ export async function resolveBotAvatar(username: string): Promise<string | null>
300333
const existing = pendingFetches.get(username);
301334
if (existing) return existing;
302335

303-
const promise = fetch(`https://api.github.com/users/${encodeURIComponent(username)}`)
336+
const promise = throttledFetch(`https://api.github.com/users/${encodeURIComponent(username)}`)
304337
.then((res) => {
305338
if (!res.ok) return null;
306339
return res.json() as Promise<{ avatar_url?: string }>;
@@ -323,11 +356,14 @@ export async function resolveBotAvatar(username: string): Promise<string | null>
323356
/**
324357
* Pre-warm the avatar cache for all bot usernames in a dataset.
325358
* Returns true if any new avatars were resolved (for triggering re-renders).
359+
* Caps at 10 API lookups per batch to avoid rate limits.
326360
*/
327361
export async function preloadBotAvatars(usernames: string[]): Promise<boolean> {
328362
const bots = usernames.filter((u) => isBot(u) && !botAvatarCache.has(u));
329363
if (bots.length === 0) return false;
330-
const results = await Promise.allSettled(bots.map(resolveBotAvatar));
364+
// Cap lookups to avoid hammering the API with many unknown bots
365+
const batch = bots.slice(0, 10);
366+
const results = await Promise.allSettled(batch.map(resolveBotAvatar));
331367
return results.some((r) => r.status === 'fulfilled' && r.value !== null);
332368
}
333369

0 commit comments

Comments
 (0)