@@ -103,6 +103,27 @@ export const SOCKET_BTM_REPO = {
103103
104104const logger = getDefaultLogger ( )
105105
106+ // In-memory TTL cache for GitHub API responses.
107+ // Prevents redundant API calls during parallel asset downloads and
108+ // repeated builds (e.g., pre-commit hooks) within the same process.
109+ const API_CACHE_TTL_MS = 5 * 60_000
110+ const _apiCache = new Map < string , { data : unknown ; expiresAt : number } > ( )
111+
112+ function getCachedApiResponse < T > ( key : string ) : T | undefined {
113+ const entry = _apiCache . get ( key )
114+ if ( entry && entry . expiresAt > Date . now ( ) ) {
115+ return entry . data as T
116+ }
117+ if ( entry ) {
118+ _apiCache . delete ( key )
119+ }
120+ return undefined
121+ }
122+
123+ function setCachedApiResponse ( key : string , data : unknown ) : void {
124+ _apiCache . set ( key , { data, expiresAt : Date . now ( ) + API_CACHE_TTL_MS } )
125+ }
126+
106127let _fs : typeof import ( 'node:fs' ) | undefined
107128let _path : typeof import ( 'node:path' ) | undefined
108129
@@ -362,32 +383,42 @@ export async function getLatestRelease(
362383 // Create matcher function if pattern provided.
363384 const isMatch = assetPattern ? createAssetMatcher ( assetPattern ) : undefined
364385
386+ const cacheKey = `releases:${ owner } /${ repo } `
387+
365388 return await pRetry (
366389 async ( ) => {
367- // Fetch recent releases (100 should cover all tool releases).
368- const response = await httpRequest (
369- `https://api.github.com/repos/${ owner } /${ repo } /releases?per_page=100` ,
370- {
371- headers : getAuthHeaders ( ) ,
372- } ,
373- )
390+ // Check in-memory cache first to avoid redundant API calls.
391+ let releases = getCachedApiResponse <
392+ Array < {
393+ tag_name : string
394+ published_at : string
395+ assets : Array < { name : string } >
396+ } >
397+ > ( cacheKey )
398+
399+ if ( ! releases ) {
400+ // Fetch recent releases (100 should cover all tool releases).
401+ const response = await httpRequest (
402+ `https://api.github.com/repos/${ owner } /${ repo } /releases?per_page=100` ,
403+ {
404+ headers : getAuthHeaders ( ) ,
405+ } ,
406+ )
374407
375- if ( ! response . ok ) {
376- throw new Error ( `Failed to fetch releases: ${ response . status } ` )
377- }
408+ if ( ! response . ok ) {
409+ throw new Error ( `Failed to fetch releases: ${ response . status } ` )
410+ }
378411
379- let releases : Array < {
380- tag_name : string
381- published_at : string
382- assets : Array < { name : string } >
383- } >
384- try {
385- releases = JSON . parse ( response . body . toString ( 'utf8' ) )
386- } catch ( cause ) {
387- throw new Error (
388- `Failed to parse GitHub releases response from https://api.github.com/repos/${ owner } /${ repo } /releases` ,
389- { cause } ,
390- )
412+ try {
413+ releases = JSON . parse ( response . body . toString ( 'utf8' ) )
414+ } catch ( cause ) {
415+ throw new Error (
416+ `Failed to parse GitHub releases response from https://api.github.com/repos/${ owner } /${ repo } /releases` ,
417+ { cause } ,
418+ )
419+ }
420+
421+ setCachedApiResponse ( cacheKey , releases )
391422 }
392423
393424 // Filter releases matching the tool prefix.
@@ -483,29 +514,37 @@ export async function getReleaseAssetUrl(
483514 ? ( input : string ) => input === assetPattern
484515 : createAssetMatcher ( assetPattern as AssetPattern )
485516
517+ const cacheKey = `release:${ owner } /${ repo } :${ tag } `
518+
486519 return await pRetry (
487520 async ( ) => {
488- const response = await httpRequest (
489- `https://api.github.com/repos/${ owner } /${ repo } /releases/tags/${ tag } ` ,
490- {
491- headers : getAuthHeaders ( ) ,
492- } ,
493- )
494-
495- if ( ! response . ok ) {
496- throw new Error ( `Failed to fetch release ${ tag } : ${ response . status } ` )
497- }
498-
499- let release : {
521+ // Check in-memory cache first to avoid redundant API calls.
522+ let release = getCachedApiResponse < {
500523 assets : Array < { name : string ; browser_download_url : string } >
501- }
502- try {
503- release = JSON . parse ( response . body . toString ( 'utf8' ) )
504- } catch ( cause ) {
505- throw new Error (
506- `Failed to parse GitHub release response for tag ${ tag } ` ,
507- { cause } ,
524+ } > ( cacheKey )
525+
526+ if ( ! release ) {
527+ const response = await httpRequest (
528+ `https://api.github.com/repos/${ owner } /${ repo } /releases/tags/${ tag } ` ,
529+ {
530+ headers : getAuthHeaders ( ) ,
531+ } ,
508532 )
533+
534+ if ( ! response . ok ) {
535+ throw new Error ( `Failed to fetch release ${ tag } : ${ response . status } ` )
536+ }
537+
538+ try {
539+ release = JSON . parse ( response . body . toString ( 'utf8' ) )
540+ } catch ( cause ) {
541+ throw new Error (
542+ `Failed to parse GitHub release response for tag ${ tag } ` ,
543+ { cause } ,
544+ )
545+ }
546+
547+ setCachedApiResponse ( cacheKey , release )
509548 }
510549
511550 // Find the matching asset.
0 commit comments