@@ -251,10 +251,6 @@ const contributionsCache = new DistributedCache<ExtendedContributionData>(1000);
251251const profileCache = new DistributedCache < GitHubUserProfile > ( 1000 ) ;
252252const reposCache = new DistributedCache < GitHubRepo [ ] > ( 500 ) ;
253253const contributedReposCache = new DistributedCache < Record < string , unknown > [ ] > ( 500 ) ;
254- const pendingContributions = new Map < string , Promise < ExtendedContributionData > > ( ) ;
255- const pendingProfiles = new Map < string , Promise < GitHubUserProfile > > ( ) ;
256- const pendingRepos = new Map < string , Promise < GitHubRepo [ ] > > ( ) ;
257- const pendingContributedRepos = new Map < string , Promise < Record < string , unknown > [ ] > > ( ) ;
258254
259255interface GitHubUserProfile {
260256 login : string ;
@@ -300,25 +296,6 @@ export function clearGitHubApiCacheForTests(): void {
300296 profileCache . clear ( ) ;
301297 reposCache . clear ( ) ;
302298 contributedReposCache . clear ( ) ;
303- pendingContributions . clear ( ) ;
304- pendingProfiles . clear ( ) ;
305- pendingRepos . clear ( ) ;
306- pendingContributedRepos . clear ( ) ;
307- }
308-
309- function dedupeRequest < T > (
310- pendingRequests : Map < string , Promise < T > > ,
311- key : string ,
312- load : ( ) => Promise < T >
313- ) : Promise < T > {
314- const pending = pendingRequests . get ( key ) ;
315- if ( pending ) return pending ;
316-
317- const request = load ( ) . finally ( ( ) => {
318- pendingRequests . delete ( key ) ;
319- } ) ;
320- pendingRequests . set ( key , request ) ;
321- return request ;
322299}
323300
324301function getGitHubToken ( ) : string {
@@ -397,31 +374,21 @@ export async function fetchGitHubContributions(
397374 options : FetchOptions = { }
398375) : Promise < ExtendedContributionData > {
399376 const key = cacheKey ( 'contributions' , username , options . from , options . to ) ;
377+ const LONG_CACHE_TTL = 7 * 24 * 60 * 60 * 1000 ;
400378
401- if ( options . bypassCache ) {
402- return fetchContributionsUncached ( username , key , options , null ) ;
403- }
404-
405- // Wrap the entire cache-check + fetch pipeline in dedupeRequest so that
406- // concurrent calls for the same key share a single in-flight promise.
407- // This closes the TOCTOU window where two requests both miss the cache
408- // (during the async DistributedCache.get) and each fires its own API call.
409- const load = async ( ) : Promise < ExtendedContributionData > => {
410- const cached = await contributionsCache . get ( key ) ;
411-
379+ const shouldFetch = ( cached : ExtendedContributionData ) => {
412380 const now = Date . now ( ) ;
413- const isStale = cached ?. calendar . lastSyncedAt
381+ return cached ?. calendar . lastSyncedAt
414382 ? now - new Date ( cached . calendar . lastSyncedAt ) . getTime ( ) > GITHUB_CACHE_TTL_MS
415383 : true ;
384+ } ;
416385
417- if ( cached && ! isStale ) {
418- return cached ;
419- }
420-
386+ const load = async ( cached : ExtendedContributionData | null ) => {
421387 return fetchContributionsUncached ( username , key , options , cached ) ;
422388 } ;
423389
424- return dedupeRequest ( pendingContributions , key , load ) ;
390+ if ( options . bypassCache ) return load ( null ) ;
391+ return contributionsCache . getOrSet ( key , load , LONG_CACHE_TTL , shouldFetch ) ;
425392}
426393
427394async function fetchContributionsUncached (
@@ -579,17 +546,12 @@ export async function fetchUserProfile(
579546 const key = cacheKey ( 'profile' , username ) ;
580547 const encodedUsername = encodeURIComponent ( username ) ;
581548
582- if ( options . bypassCache ) {
583- return fetchProfileUncached ( encodedUsername , key , options ) ;
584- }
585-
586- const load = async ( ) : Promise < GitHubUserProfile > => {
587- const cached = await profileCache . get ( key ) ;
588- if ( cached ) return cached ;
549+ const load = async ( ) => {
589550 return fetchProfileUncached ( encodedUsername , key , options ) ;
590551 } ;
591552
592- return dedupeRequest ( pendingProfiles , key , load ) ;
553+ if ( options . bypassCache ) return load ( ) ;
554+ return profileCache . getOrSet ( key , load , GITHUB_CACHE_TTL_MS ) ;
593555}
594556
595557async function fetchProfileUncached (
@@ -627,17 +589,12 @@ export async function fetchUserRepos(
627589 const key = cacheKey ( 'repos' , username ) ;
628590 const encodedUsername = encodeURIComponent ( username ) ;
629591
630- if ( options . bypassCache ) {
631- return fetchReposUncached ( encodedUsername , key , options ) ;
632- }
633-
634- const load = async ( ) : Promise < GitHubRepo [ ] > => {
635- const cached = await reposCache . get ( key ) ;
636- if ( cached ) return cached ;
592+ const load = async ( ) => {
637593 return fetchReposUncached ( encodedUsername , key , options ) ;
638594 } ;
639595
640- return dedupeRequest ( pendingRepos , key , load ) ;
596+ if ( options . bypassCache ) return load ( ) ;
597+ return reposCache . getOrSet ( key , load , GITHUB_CACHE_TTL_MS ) ;
641598}
642599
643600async function fetchReposUncached (
@@ -945,10 +902,6 @@ export async function fetchContributedRepos(
945902 options : FetchOptions = { }
946903) : Promise < Record < string , unknown > [ ] > {
947904 const key = cacheKey ( 'repos:contributed' , username ) ;
948- if ( ! options . bypassCache ) {
949- const cached = await contributedReposCache . get ( key ) ;
950- if ( cached ) return cached ;
951- }
952905
953906 const load = async ( ) => {
954907 const query = `
@@ -983,15 +936,11 @@ export async function fetchContributedRepos(
983936 if ( ! res . ok ) return [ ] ;
984937 const data = await res . json ( ) ;
985938 const result = data ?. data ?. user ?. repositoriesContributedTo ?. nodes || [ ] ;
986-
987- if ( ! options . bypassCache ) {
988- await contributedReposCache . set ( key , result , GITHUB_CACHE_TTL_MS ) ;
989- }
990939 return result ;
991940 } ;
992941
993942 if ( options . bypassCache ) return load ( ) ;
994- return dedupeRequest ( pendingContributedRepos , key , load ) ;
943+ return contributedReposCache . getOrSet ( key , load , GITHUB_CACHE_TTL_MS ) ;
995944}
996945
997946export interface DeveloperScoreInput {
0 commit comments