@@ -81,6 +81,24 @@ interface CachedUserRepos {
8181const userReposCache = new Map < string , CachedUserRepos > ( ) ;
8282const USER_REPOS_CACHE_TTL_MS = 10 * 60 * 1000 ; // 10 minutes - hard expiry
8383const STALE_WHILE_REVALIDATE_MS = 5 * 60 * 1000 ; // Trigger background refresh after 5 minutes
84+ const MAX_CACHE_ENTRIES = 500 ; // Prevent unbounded growth
85+
86+ /**
87+ * Evict oldest cache entries if we exceed the limit
88+ */
89+ function evictOldestCacheEntries ( ) : void {
90+ if ( userReposCache . size <= MAX_CACHE_ENTRIES ) return ;
91+
92+ // Convert to array, sort by cachedAt (oldest first), delete oldest entries
93+ const entries = Array . from ( userReposCache . entries ( ) )
94+ . sort ( ( a , b ) => a [ 1 ] . cachedAt - b [ 1 ] . cachedAt ) ;
95+
96+ const toEvict = entries . slice ( 0 , entries . length - MAX_CACHE_ENTRIES ) ;
97+ for ( const [ key ] of toEvict ) {
98+ console . log ( `[repos-cache] Evicting oldest cache entry: ${ key . substring ( 0 , 8 ) } ` ) ;
99+ userReposCache . delete ( key ) ;
100+ }
101+ }
84102
85103/**
86104 * Background refresh function that paginates through ALL user repos
@@ -144,6 +162,7 @@ async function refreshUserReposInBackground(nangoConnectionId: string): Promise<
144162 isComplete : true ,
145163 refreshInProgress : false ,
146164 } ) ;
165+ evictOldestCacheEntries ( ) ;
147166 } catch ( err ) {
148167 console . error ( `[repos-cache] Background refresh failed for ${ nangoConnectionId . substring ( 0 , 8 ) } :` , err ) ;
149168
@@ -182,81 +201,105 @@ function getCachedUserRepos(nangoConnectionId: string): CachedUserRepos | null {
182201 return cached ;
183202}
184203
204+ // Track in-flight initializations to prevent duplicate API calls
205+ const initializingConnections = new Set < string > ( ) ;
206+
185207/**
186208 * Initialize cache with first page and trigger background refresh for rest
187209 * Returns the first page of repos immediately
188210 */
189211async function initializeUserReposCache ( nangoConnectionId : string ) : Promise < CachedRepo [ ] > {
190- // Fetch first page synchronously
191- const firstPage = await nangoService . listUserAccessibleRepos ( nangoConnectionId , {
192- perPage : 100 ,
193- page : 1 ,
194- type : 'all' ,
195- } ) ;
196-
197- const repos = firstPage . repositories . map ( r => ( {
198- fullName : r . fullName ,
199- permissions : r . permissions ,
200- } ) ) ;
201-
202- // Store first page immediately
203- userReposCache . set ( nangoConnectionId , {
204- repositories : repos ,
205- cachedAt : Date . now ( ) ,
206- isComplete : ! firstPage . hasMore ,
207- refreshInProgress : firstPage . hasMore , // Will be refreshing if there's more
208- } ) ;
209-
210- // If there are more pages, trigger background refresh to get them all
211- if ( firstPage . hasMore ) {
212- console . log ( `[repos-cache] First page has ${ repos . length } repos, more available - triggering background refresh` ) ;
213- // Fire and forget - continue pagination in background
214- ( async ( ) => {
215- try {
216- const allRepos = [ ...repos ] ;
217- let page = 2 ;
218- let hasMore = true ;
219- const MAX_PAGES = 20 ;
220-
221- while ( hasMore && page <= MAX_PAGES ) {
222- const result = await nangoService . listUserAccessibleRepos ( nangoConnectionId , {
223- perPage : 100 ,
224- page,
225- type : 'all' ,
226- } ) ;
212+ // Check if another request is already initializing this connection
213+ if ( initializingConnections . has ( nangoConnectionId ) ) {
214+ console . log ( `[repos-cache] Another request is initializing ${ nangoConnectionId . substring ( 0 , 8 ) } , waiting...` ) ;
215+ // Wait a bit and check cache again
216+ await new Promise ( resolve => setTimeout ( resolve , 500 ) ) ;
217+ const cached = userReposCache . get ( nangoConnectionId ) ;
218+ if ( cached ) {
219+ return cached . repositories ;
220+ }
221+ // Still no cache, fall through to initialize (previous request may have failed)
222+ }
227223
228- allRepos . push ( ...result . repositories . map ( r => ( {
229- fullName : r . fullName ,
230- permissions : r . permissions ,
231- } ) ) ) ;
224+ initializingConnections . add ( nangoConnectionId ) ;
225+
226+ try {
227+ // Fetch first page synchronously
228+ const firstPage = await nangoService . listUserAccessibleRepos ( nangoConnectionId , {
229+ perPage : 100 ,
230+ page : 1 ,
231+ type : 'all' ,
232+ } ) ;
232233
233- hasMore = result . hasMore ;
234- page ++ ;
234+ const repos = firstPage . repositories . map ( r => ( {
235+ fullName : r . fullName ,
236+ permissions : r . permissions ,
237+ } ) ) ;
235238
236- if ( hasMore ) {
237- await new Promise ( resolve => setTimeout ( resolve , 100 ) ) ;
239+ // Store first page immediately
240+ userReposCache . set ( nangoConnectionId , {
241+ repositories : repos ,
242+ cachedAt : Date . now ( ) ,
243+ isComplete : ! firstPage . hasMore ,
244+ refreshInProgress : firstPage . hasMore , // Will be refreshing if there's more
245+ } ) ;
246+ evictOldestCacheEntries ( ) ;
247+
248+ // If there are more pages, trigger background refresh to get the rest
249+ if ( firstPage . hasMore ) {
250+ console . log ( `[repos-cache] First page has ${ repos . length } repos, more available - triggering background pagination` ) ;
251+ // Fire and forget - reuse the shared background refresh function
252+ // But start from page 2 with the existing repos
253+ ( async ( ) => {
254+ try {
255+ const allRepos = [ ...repos ] ;
256+ let page = 2 ;
257+ let hasMore = true ;
258+ const MAX_PAGES = 20 ;
259+
260+ while ( hasMore && page <= MAX_PAGES ) {
261+ const result = await nangoService . listUserAccessibleRepos ( nangoConnectionId , {
262+ perPage : 100 ,
263+ page,
264+ type : 'all' ,
265+ } ) ;
266+
267+ allRepos . push ( ...result . repositories . map ( r => ( {
268+ fullName : r . fullName ,
269+ permissions : r . permissions ,
270+ } ) ) ) ;
271+
272+ hasMore = result . hasMore ;
273+ page ++ ;
274+
275+ if ( hasMore ) {
276+ await new Promise ( resolve => setTimeout ( resolve , 100 ) ) ;
277+ }
238278 }
239- }
240279
241- console . log ( `[repos-cache] Background pagination complete: ${ allRepos . length } total repos` ) ;
280+ console . log ( `[repos-cache] Background pagination complete: ${ allRepos . length } total repos` ) ;
242281
243- userReposCache . set ( nangoConnectionId , {
244- repositories : allRepos ,
245- cachedAt : Date . now ( ) ,
246- isComplete : true ,
247- refreshInProgress : false ,
248- } ) ;
249- } catch ( err ) {
250- console . error ( '[repos-cache] Background pagination failed:' , err ) ;
251- const existing = userReposCache . get ( nangoConnectionId ) ;
252- if ( existing ) {
253- existing . refreshInProgress = false ;
282+ userReposCache . set ( nangoConnectionId , {
283+ repositories : allRepos ,
284+ cachedAt : Date . now ( ) ,
285+ isComplete : true ,
286+ refreshInProgress : false ,
287+ } ) ;
288+ evictOldestCacheEntries ( ) ;
289+ } catch ( err ) {
290+ console . error ( '[repos-cache] Background pagination failed:' , err ) ;
291+ const existing = userReposCache . get ( nangoConnectionId ) ;
292+ if ( existing ) {
293+ existing . refreshInProgress = false ;
294+ }
254295 }
255- }
256- } ) ( ) ;
257- }
296+ } ) ( ) ;
297+ }
258298
259- return repos ;
299+ return repos ;
300+ } finally {
301+ initializingConnections . delete ( nangoConnectionId ) ;
302+ }
260303}
261304
262305// ============================================================================
0 commit comments