@@ -288,6 +288,39 @@ const HARDCODED_BOTS = new Set(botAvatarCache.keys());
288288/** In-flight fetch promises to avoid duplicate API calls */
289289const 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 */
327361export 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