@@ -15,6 +15,7 @@ import {
1515 type SliceType ,
1616 type UploadInput ,
1717 type UploadOptions ,
18+ type StallDetectionOptions ,
1819} from './options.js'
1920import { uuid } from './uuid.js'
2021
@@ -54,6 +55,8 @@ export const defaultOptions = {
5455 httpStack : undefined ,
5556
5657 protocol : PROTOCOL_TUS_V1 as UploadOptions [ 'protocol' ] ,
58+
59+ stallDetection : undefined ,
5760}
5861
5962export class BaseUpload {
@@ -105,6 +108,13 @@ export class BaseUpload {
105108 // parts, if the parallelUploads option is used.
106109 private _parallelUploadUrls ?: string [ ]
107110
111+ // Stall detection properties
112+ private _lastProgress = 0
113+ private _lastProgressTime = 0
114+ private _uploadStartTime = 0
115+ private _stallCheckInterval ?: ReturnType < typeof setTimeout >
116+ private _hasProgressEvents = false
117+
108118 constructor ( file : UploadInput , options : UploadOptions ) {
109119 // Warn about removed options from previous versions
110120 if ( 'resume' in options ) {
@@ -121,6 +131,9 @@ export class BaseUpload {
121131 this . options . chunkSize = Number ( this . options . chunkSize )
122132
123133 this . file = file
134+
135+ // Initialize stall detection options
136+ this . options . stallDetection = this . _getStallDetectionDefaults ( options . stallDetection )
124137 }
125138
126139 async findPreviousUploads ( ) : Promise < PreviousUpload [ ] > {
@@ -262,6 +275,9 @@ export class BaseUpload {
262275 } else {
263276 await this . _startSingleUpload ( )
264277 }
278+
279+ // Setup stall detection
280+ this . _setupStallDetection ( )
265281 }
266282
267283 /**
@@ -457,6 +473,9 @@ export class BaseUpload {
457473 // Set the aborted flag before any `await`s, so no new requests are started.
458474 this . _aborted = true
459475
476+ // Clear any stall detection
477+ this . _clearStallDetection ( )
478+
460479 // Stop any parallel partial uploads, that have been started in _startParallelUploads.
461480 if ( this . _parallelUploads != null ) {
462481 for ( const upload of this . _parallelUploads ) {
@@ -551,6 +570,12 @@ export class BaseUpload {
551570 * @api private
552571 */
553572 private _emitProgress ( bytesSent : number , bytesTotal : number | null ) : void {
573+ // Update stall detection state if progress has been made
574+ if ( bytesSent > this . _lastProgress ) {
575+ this . _lastProgress = bytesSent
576+ this . _lastProgressTime = Date . now ( )
577+ }
578+
554579 if ( typeof this . options . onProgress === 'function' ) {
555580 this . options . onProgress ( bytesSent , bytesTotal )
556581 }
@@ -985,6 +1010,137 @@ export class BaseUpload {
9851010 _sendRequest ( req : HttpRequest , body ?: SliceType ) : Promise < HttpResponse > {
9861011 return sendRequest ( req , body , this . options )
9871012 }
1013+
1014+ /**
1015+ * Apply default stall detection options
1016+ */
1017+ private _getStallDetectionDefaults (
1018+ options ?: Partial < StallDetectionOptions >
1019+ ) : StallDetectionOptions {
1020+ return {
1021+ enabled : options ?. enabled ?? true ,
1022+ stallTimeout : options ?. stallTimeout ?? 30000 ,
1023+ checkInterval : options ?. checkInterval ?? 5000 ,
1024+ minimumBytesPerSecond : options ?. minimumBytesPerSecond ?? 1
1025+ }
1026+ }
1027+
1028+ /**
1029+ * Detect if current HttpStack supports progress events
1030+ */
1031+ private _supportsProgressEvents ( ) : boolean {
1032+ const httpStack = this . options . httpStack
1033+ // Check if getName method exists and if it returns one of our known stacks
1034+ return typeof httpStack . getName === 'function' &&
1035+ [ "NodeHttpStack" , "XHRHttpStack" ] . includes ( httpStack . getName ( ) )
1036+ }
1037+
1038+ /**
1039+ * Check if upload has stalled based on progress events
1040+ */
1041+ private _isProgressStalled ( now : number ) : boolean {
1042+ const stallDetection = this . options . stallDetection
1043+ if ( ! stallDetection ) return false
1044+
1045+ const timeSinceProgress = now - this . _lastProgressTime
1046+ const stallTimeout = stallDetection . stallTimeout ?? 30000
1047+ const isStalled = timeSinceProgress > stallTimeout
1048+
1049+ if ( isStalled ) {
1050+ log ( `No progress for ${ timeSinceProgress } ms (limit: ${ stallTimeout } ms)` )
1051+ }
1052+
1053+ return isStalled
1054+ }
1055+
1056+ /**
1057+ * Check if upload has stalled based on transfer rate
1058+ */
1059+ private _isTransferRateStalled ( now : number ) : boolean {
1060+ const stallDetection = this . options . stallDetection
1061+ if ( ! stallDetection ) return false
1062+
1063+ const totalTime = Math . max ( ( now - this . _uploadStartTime ) / 1000 , 0.001 ) // in seconds, prevent division by zero
1064+ const bytesPerSecond = this . _offset / totalTime
1065+
1066+ // Need grace period for initial connection setup (5 seconds)
1067+ const hasGracePeriodPassed = totalTime > 5
1068+ const minBytes = stallDetection . minimumBytesPerSecond ?? 1
1069+ const isStalled = hasGracePeriodPassed && bytesPerSecond < minBytes
1070+
1071+ if ( isStalled ) {
1072+ log ( `Transfer rate too low: ${ bytesPerSecond . toFixed ( 2 ) } bytes/sec (minimum: ${ minBytes } bytes/sec)` )
1073+ }
1074+
1075+ return isStalled
1076+ }
1077+
1078+ /**
1079+ * Handle a detected stall by forcing a retry
1080+ */
1081+ private _handleStall ( reason : string ) : void {
1082+ log ( `Upload stalled: ${ reason } ` )
1083+
1084+ this . _clearStallDetection ( )
1085+
1086+ // If using parallel uploads, abort them all
1087+ if ( this . _parallelUploads ) {
1088+ for ( const upload of this . _parallelUploads ) {
1089+ upload . abort ( )
1090+ }
1091+ } else if ( this . _req ) {
1092+ // For single uploads, abort the current request
1093+ this . _req . abort ( )
1094+ }
1095+
1096+ // Force a retry via the error mechanism
1097+ this . _retryOrEmitError ( new Error ( `Upload stalled: ${ reason } ` ) )
1098+ }
1099+
1100+ /**
1101+ * Clear stall detection timer if running
1102+ */
1103+ private _clearStallDetection ( ) : void {
1104+ if ( this . _stallCheckInterval ) {
1105+ clearInterval ( this . _stallCheckInterval )
1106+ this . _stallCheckInterval = undefined
1107+ }
1108+ }
1109+
1110+ /**
1111+ * Setup stall detection monitoring
1112+ */
1113+ private _setupStallDetection ( ) : void {
1114+ const stallDetection = this . options . stallDetection
1115+
1116+ // Early return if disabled or undefined
1117+ if ( ! stallDetection ?. enabled ) {
1118+ return
1119+ }
1120+
1121+ // Initialize state
1122+ this . _uploadStartTime = Date . now ( )
1123+ this . _lastProgressTime = Date . now ( )
1124+ this . _hasProgressEvents = this . _supportsProgressEvents ( )
1125+ this . _clearStallDetection ( )
1126+
1127+ // Setup periodic check with default interval of 5000ms if undefined
1128+ this . _stallCheckInterval = setInterval ( ( ) => {
1129+ // Skip check if already aborted
1130+ if ( this . _aborted ) {
1131+ return
1132+ }
1133+
1134+ const now = Date . now ( )
1135+
1136+ // Different stall detection based on stack capabilities
1137+ if ( this . _hasProgressEvents && this . _isProgressStalled ( now ) ) {
1138+ this . _handleStall ( "No progress events received" )
1139+ } else if ( ! this . _hasProgressEvents && this . _isTransferRateStalled ( now ) ) {
1140+ this . _handleStall ( "Transfer rate too low" )
1141+ }
1142+ } , stallDetection . checkInterval ?? 5000 )
1143+ }
9881144}
9891145
9901146function encodeMetadata ( metadata : Record < string , string > ) : string {
0 commit comments