@@ -640,16 +640,26 @@ function createProgressDisplay(
640640type ProgressCallback = (label: string, downloaded: number, total: number, speed: number, done: boolean, elapsed: number) => void;
641641*/
642642
643- async function download (
643+ const DOWNLOAD_MAX_ATTEMPTS = 3 ;
644+ const DOWNLOAD_RETRY_BASE_DELAY_MS = 1000 ;
645+ const DOWNLOAD_IDLE_TIMEOUT_MS = 60_000 ;
646+
647+ function isRetryableDownloadError ( error /*: Error */ ) /*: boolean */ {
648+ // Client errors are deterministic except for request timeout / rate limiting.
649+ // Network and stream errors do not carry an HTTP prefix and are safe to retry.
650+ const httpStatus = / ^ H T T P ( \d { 3 } ) \b / . exec ( error . message ) ?. [ 1 ] ;
651+ if ( httpStatus == null ) {
652+ return true ;
653+ }
654+ const status = Number ( httpStatus ) ;
655+ return status === 408 || status === 429 || status >= 500 ;
656+ }
657+
658+ async function downloadOnce (
644659 url /*: string */ ,
645660 destPath /*: string */ ,
646661 onProgress /*:: ?: ProgressCallback */ ,
647662) /*: Promise<void> */ {
648- if ( fs . existsSync ( destPath ) ) {
649- log ( ` Already cached: ${ path . basename ( destPath ) } ` ) ;
650- return ;
651- }
652-
653663 const tmpPath = destPath + '.download' ;
654664 try {
655665 // $FlowFixMe[incompatible-call] global fetch not in Flow stubs
@@ -662,6 +672,7 @@ async function download(
662672 let downloadedBytes = 0 ;
663673 let lastPrintTime = Date . now ( ) ;
664674 let lastPrintBytes = 0 ;
675+ let lastDataTime = Date . now ( ) ;
665676 const startTime = Date . now ( ) ;
666677
667678 const fileStream = fs . createWriteStream ( tmpPath ) ;
@@ -719,14 +730,25 @@ async function download(
719730 await new Promise ( ( resolve , reject ) => {
720731 let progressInterval ;
721732 try {
722- progressInterval = setInterval ( ( ) => reportProgress ( ) , 500 ) ;
733+ progressInterval = setInterval ( ( ) => {
734+ reportProgress ( ) ;
735+ if ( Date . now ( ) - lastDataTime >= DOWNLOAD_IDLE_TIMEOUT_MS ) {
736+ nodeReadable . destroy (
737+ new Error (
738+ `Download made no progress for ${ DOWNLOAD_IDLE_TIMEOUT_MS / 1000 } s: ${ url } ` ,
739+ ) ,
740+ ) ;
741+ }
742+ } , 500 ) ;
723743
724744 nodeReadable
725745 . on ( 'data' , chunk => {
726746 downloadedBytes += chunk . length ;
747+ lastDataTime = Date . now ( ) ;
727748 } )
728749 . on ( 'error' , err => {
729750 clearInterval ( progressInterval ) ;
751+ fileStream . destroy ( ) ;
730752 reject ( err ) ;
731753 } )
732754 . pipe ( fileStream )
@@ -737,10 +759,13 @@ async function download(
737759 } )
738760 . on ( 'error' , err => {
739761 clearInterval ( progressInterval ) ;
762+ nodeReadable . destroy ( ) ;
740763 reject ( err ) ;
741764 } ) ;
742765 } catch ( err ) {
743766 if ( progressInterval != null ) clearInterval ( progressInterval ) ;
767+ nodeReadable . destroy ( ) ;
768+ fileStream . destroy ( ) ;
744769 reject ( err ) ;
745770 }
746771 } ) ;
@@ -753,7 +778,7 @@ async function download(
753778
754779 fs . renameSync ( tmpPath , destPath ) ;
755780 } catch ( err ) {
756- // Clean up partial .download temp file on failure
781+ // Clean up partial .download temp file on failure.
757782 try {
758783 fs . unlinkSync ( tmpPath ) ;
759784 } catch {
@@ -763,6 +788,37 @@ async function download(
763788 }
764789}
765790
791+ async function download (
792+ url /*: string */ ,
793+ destPath /*: string */ ,
794+ onProgress /*:: ?: ProgressCallback */ ,
795+ ) /*: Promise<void> */ {
796+ if ( fs . existsSync ( destPath ) ) {
797+ log ( ` Already cached: ${ path . basename ( destPath ) } ` ) ;
798+ return ;
799+ }
800+
801+ for ( let attempt = 1 ; attempt <= DOWNLOAD_MAX_ATTEMPTS ; attempt ++ ) {
802+ try {
803+ await downloadOnce ( url , destPath , onProgress ) ;
804+ return ;
805+ } catch ( error ) {
806+ if (
807+ attempt === DOWNLOAD_MAX_ATTEMPTS ||
808+ ! isRetryableDownloadError ( error )
809+ ) {
810+ throw error ;
811+ }
812+ const delayMs = DOWNLOAD_RETRY_BASE_DELAY_MS * attempt ;
813+ warn (
814+ ` Download failed (${ error . message } ); retrying in ${ delayMs } ms ` +
815+ `(attempt ${ attempt + 1 } /${ DOWNLOAD_MAX_ATTEMPTS } )` ,
816+ ) ;
817+ await new Promise ( resolve => setTimeout ( resolve , delayMs ) ) ;
818+ }
819+ }
820+ }
821+
766822/**
767823 * Extracts a .tar.gz and returns the path to the first .xcframework found.
768824 */
@@ -1544,6 +1600,7 @@ module.exports = {
15441600 resolveLatestV1Version,
15451601 resolveRNCoreArtifact,
15461602 resolveRNDepsArtifact,
1603+ download,
15471604 exists,
15481605 formatBytes,
15491606 formatSpeed,
0 commit comments