@@ -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 /**
@@ -337,6 +353,10 @@ export class BaseUpload {
337353 if ( totalSize == null ) {
338354 throw new Error ( 'tus: Expected totalSize to be set' )
339355 }
356+
357+ // Update progress timestamp for the parallel upload to track stalls
358+ upload . _lastProgressTime = Date . now ( )
359+
340360 this . _emitProgress ( totalProgress , totalSize )
341361 } ,
342362 // Wait until every partial upload has an upload URL, so we can add
@@ -457,6 +477,9 @@ export class BaseUpload {
457477 // Set the aborted flag before any `await`s, so no new requests are started.
458478 this . _aborted = true
459479
480+ // Clear any stall detection
481+ this . _clearStallDetection ( )
482+
460483 // Stop any parallel partial uploads, that have been started in _startParallelUploads.
461484 if ( this . _parallelUploads != null ) {
462485 for ( const upload of this . _parallelUploads ) {
@@ -551,6 +574,12 @@ export class BaseUpload {
551574 * @api private
552575 */
553576 private _emitProgress ( bytesSent : number , bytesTotal : number | null ) : void {
577+ // Update stall detection state if progress has been made
578+ if ( bytesSent > this . _lastProgress ) {
579+ this . _lastProgress = bytesSent
580+ this . _lastProgressTime = Date . now ( )
581+ }
582+
554583 if ( typeof this . options . onProgress === 'function' ) {
555584 this . options . onProgress ( bytesSent , bytesTotal )
556585 }
@@ -985,6 +1014,133 @@ export class BaseUpload {
9851014 _sendRequest ( req : HttpRequest , body ?: SliceType ) : Promise < HttpResponse > {
9861015 return sendRequest ( req , body , this . options )
9871016 }
1017+
1018+ /**
1019+ * Apply default stall detection options
1020+ */
1021+ private _getStallDetectionDefaults (
1022+ options ?: Partial < StallDetectionOptions >
1023+ ) : StallDetectionOptions {
1024+ return {
1025+ enabled : options ?. enabled ?? true ,
1026+ stallTimeout : options ?. stallTimeout ?? 30000 ,
1027+ checkInterval : options ?. checkInterval ?? 5000 ,
1028+ minimumBytesPerSecond : options ?. minimumBytesPerSecond ?? 1
1029+ }
1030+ }
1031+
1032+ /**
1033+ * Detect if current HttpStack supports progress events
1034+ */
1035+ private _supportsProgressEvents ( ) : boolean {
1036+ const httpStack = this . options . httpStack
1037+ // Check if getName method exists and if it returns one of our known stacks
1038+ return typeof httpStack . getName === 'function' &&
1039+ [ "NodeHttpStack" , "XHRHttpStack" ] . includes ( httpStack . getName ( ) )
1040+ }
1041+
1042+ /**
1043+ * Check if upload has stalled based on progress events
1044+ */
1045+ private _isProgressStalled ( now : number ) : boolean {
1046+ const stallDetection = this . options . stallDetection
1047+ if ( ! stallDetection ) return false
1048+
1049+ const timeSinceProgress = now - this . _lastProgressTime
1050+ const stallTimeout = stallDetection . stallTimeout ?? 30000
1051+ const isStalled = timeSinceProgress > stallTimeout
1052+
1053+ if ( isStalled ) {
1054+ log ( `No progress for ${ timeSinceProgress } ms (limit: ${ stallTimeout } ms)` )
1055+ }
1056+
1057+ return isStalled
1058+ }
1059+
1060+ /**
1061+ * Check if upload has stalled based on transfer rate
1062+ */
1063+ private _isTransferRateStalled ( now : number ) : boolean {
1064+ const stallDetection = this . options . stallDetection
1065+ if ( ! stallDetection ) return false
1066+
1067+ const totalTime = Math . max ( ( now - this . _uploadStartTime ) / 1000 , 0.001 ) // in seconds, prevent division by zero
1068+ const bytesPerSecond = this . _offset / totalTime
1069+
1070+ // Need grace period for initial connection setup (5 seconds)
1071+ const hasGracePeriodPassed = totalTime > 5
1072+ const minBytes = stallDetection . minimumBytesPerSecond ?? 1
1073+ const isStalled = hasGracePeriodPassed && bytesPerSecond < minBytes
1074+
1075+ if ( isStalled ) {
1076+ log ( `Transfer rate too low: ${ bytesPerSecond . toFixed ( 2 ) } bytes/sec (minimum: ${ minBytes } bytes/sec)` )
1077+ }
1078+
1079+ return isStalled
1080+ }
1081+
1082+ /**
1083+ * Handle a detected stall by forcing a retry
1084+ */
1085+ private _handleStall ( reason : string ) : void {
1086+ log ( `Upload stalled: ${ reason } ` )
1087+
1088+ this . _clearStallDetection ( )
1089+
1090+ // Just abort the current request, not the entire upload
1091+ // Each parallel upload instance has its own stall detection
1092+ if ( this . _req ) {
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