@@ -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 {
@@ -109,6 +112,13 @@ export class BaseUpload {
109112 // upload options or HEAD response)
110113 private _uploadLengthDeferred : boolean
111114
115+ // Stall detection properties
116+ private _lastProgress = 0
117+ private _lastProgressTime = 0
118+ private _uploadStartTime = 0
119+ private _stallCheckInterval ?: ReturnType < typeof setTimeout >
120+ private _hasProgressEvents = false
121+
112122 constructor ( file : UploadInput , options : UploadOptions ) {
113123 // Warn about removed options from previous versions
114124 if ( 'resume' in options ) {
@@ -127,6 +137,9 @@ export class BaseUpload {
127137 this . _uploadLengthDeferred = this . options . uploadLengthDeferred
128138
129139 this . file = file
140+
141+ // Initialize stall detection options
142+ this . options . stallDetection = this . _getStallDetectionDefaults ( options . stallDetection )
130143 }
131144
132145 async findPreviousUploads ( ) : Promise < PreviousUpload [ ] > {
@@ -268,6 +281,9 @@ export class BaseUpload {
268281 } else {
269282 await this . _startSingleUpload ( )
270283 }
284+
285+ // Setup stall detection
286+ this . _setupStallDetection ( )
271287 }
272288
273289 /**
@@ -343,6 +359,10 @@ export class BaseUpload {
343359 if ( totalSize == null ) {
344360 throw new Error ( 'tus: Expected totalSize to be set' )
345361 }
362+
363+ // Update progress timestamp for the parallel upload to track stalls
364+ upload . _lastProgressTime = Date . now ( )
365+
346366 this . _emitProgress ( totalProgress , totalSize )
347367 } ,
348368 // Wait until every partial upload has an upload URL, so we can add
@@ -463,6 +483,9 @@ export class BaseUpload {
463483 // Set the aborted flag before any `await`s, so no new requests are started.
464484 this . _aborted = true
465485
486+ // Clear any stall detection
487+ this . _clearStallDetection ( )
488+
466489 // Stop any parallel partial uploads, that have been started in _startParallelUploads.
467490 if ( this . _parallelUploads != null ) {
468491 for ( const upload of this . _parallelUploads ) {
@@ -557,6 +580,12 @@ export class BaseUpload {
557580 * @api private
558581 */
559582 private _emitProgress ( bytesSent : number , bytesTotal : number | null ) : void {
583+ // Update stall detection state if progress has been made
584+ if ( bytesSent > this . _lastProgress ) {
585+ this . _lastProgress = bytesSent
586+ this . _lastProgressTime = Date . now ( )
587+ }
588+
560589 if ( typeof this . options . onProgress === 'function' ) {
561590 this . options . onProgress ( bytesSent , bytesTotal )
562591 }
@@ -995,6 +1024,133 @@ export class BaseUpload {
9951024 _sendRequest ( req : HttpRequest , body ?: SliceType ) : Promise < HttpResponse > {
9961025 return sendRequest ( req , body , this . options )
9971026 }
1027+
1028+ /**
1029+ * Apply default stall detection options
1030+ */
1031+ private _getStallDetectionDefaults (
1032+ options ?: Partial < StallDetectionOptions >
1033+ ) : StallDetectionOptions {
1034+ return {
1035+ enabled : options ?. enabled ?? true ,
1036+ stallTimeout : options ?. stallTimeout ?? 30000 ,
1037+ checkInterval : options ?. checkInterval ?? 5000 ,
1038+ minimumBytesPerSecond : options ?. minimumBytesPerSecond ?? 1
1039+ }
1040+ }
1041+
1042+ /**
1043+ * Detect if current HttpStack supports progress events
1044+ */
1045+ private _supportsProgressEvents ( ) : boolean {
1046+ const httpStack = this . options . httpStack
1047+ // Check if getName method exists and if it returns one of our known stacks
1048+ return typeof httpStack . getName === 'function' &&
1049+ [ "NodeHttpStack" , "XHRHttpStack" ] . includes ( httpStack . getName ( ) )
1050+ }
1051+
1052+ /**
1053+ * Check if upload has stalled based on progress events
1054+ */
1055+ private _isProgressStalled ( now : number ) : boolean {
1056+ const stallDetection = this . options . stallDetection
1057+ if ( ! stallDetection ) return false
1058+
1059+ const timeSinceProgress = now - this . _lastProgressTime
1060+ const stallTimeout = stallDetection . stallTimeout ?? 30000
1061+ const isStalled = timeSinceProgress > stallTimeout
1062+
1063+ if ( isStalled ) {
1064+ log ( `No progress for ${ timeSinceProgress } ms (limit: ${ stallTimeout } ms)` )
1065+ }
1066+
1067+ return isStalled
1068+ }
1069+
1070+ /**
1071+ * Check if upload has stalled based on transfer rate
1072+ */
1073+ private _isTransferRateStalled ( now : number ) : boolean {
1074+ const stallDetection = this . options . stallDetection
1075+ if ( ! stallDetection ) return false
1076+
1077+ const totalTime = Math . max ( ( now - this . _uploadStartTime ) / 1000 , 0.001 ) // in seconds, prevent division by zero
1078+ const bytesPerSecond = this . _offset / totalTime
1079+
1080+ // Need grace period for initial connection setup (5 seconds)
1081+ const hasGracePeriodPassed = totalTime > 5
1082+ const minBytes = stallDetection . minimumBytesPerSecond ?? 1
1083+ const isStalled = hasGracePeriodPassed && bytesPerSecond < minBytes
1084+
1085+ if ( isStalled ) {
1086+ log ( `Transfer rate too low: ${ bytesPerSecond . toFixed ( 2 ) } bytes/sec (minimum: ${ minBytes } bytes/sec)` )
1087+ }
1088+
1089+ return isStalled
1090+ }
1091+
1092+ /**
1093+ * Handle a detected stall by forcing a retry
1094+ */
1095+ private _handleStall ( reason : string ) : void {
1096+ log ( `Upload stalled: ${ reason } ` )
1097+
1098+ this . _clearStallDetection ( )
1099+
1100+ // Just abort the current request, not the entire upload
1101+ // Each parallel upload instance has its own stall detection
1102+ if ( this . _req ) {
1103+ this . _req . abort ( )
1104+ }
1105+
1106+ // Force a retry via the error mechanism
1107+ this . _retryOrEmitError ( new Error ( `Upload stalled: ${ reason } ` ) )
1108+ }
1109+
1110+ /**
1111+ * Clear stall detection timer if running
1112+ */
1113+ private _clearStallDetection ( ) : void {
1114+ if ( this . _stallCheckInterval ) {
1115+ clearInterval ( this . _stallCheckInterval )
1116+ this . _stallCheckInterval = undefined
1117+ }
1118+ }
1119+
1120+ /**
1121+ * Setup stall detection monitoring
1122+ */
1123+ private _setupStallDetection ( ) : void {
1124+ const stallDetection = this . options . stallDetection
1125+
1126+ // Early return if disabled or undefined
1127+ if ( ! stallDetection ?. enabled ) {
1128+ return
1129+ }
1130+
1131+ // Initialize state
1132+ this . _uploadStartTime = Date . now ( )
1133+ this . _lastProgressTime = Date . now ( )
1134+ this . _hasProgressEvents = this . _supportsProgressEvents ( )
1135+ this . _clearStallDetection ( )
1136+
1137+ // Setup periodic check with default interval of 5000ms if undefined
1138+ this . _stallCheckInterval = setInterval ( ( ) => {
1139+ // Skip check if already aborted
1140+ if ( this . _aborted ) {
1141+ return
1142+ }
1143+
1144+ const now = Date . now ( )
1145+
1146+ // Different stall detection based on stack capabilities
1147+ if ( this . _hasProgressEvents && this . _isProgressStalled ( now ) ) {
1148+ this . _handleStall ( "No progress events received" )
1149+ } else if ( ! this . _hasProgressEvents && this . _isTransferRateStalled ( now ) ) {
1150+ this . _handleStall ( "Transfer rate too low" )
1151+ }
1152+ } , stallDetection . checkInterval ?? 5000 )
1153+ }
9981154}
9991155
10001156function encodeMetadata ( metadata : Record < string , string > ) : string {
0 commit comments