@@ -15,6 +15,7 @@ import {
15
15
type SliceType ,
16
16
type UploadInput ,
17
17
type UploadOptions ,
18
+ type StallDetectionOptions ,
18
19
} from './options.js'
19
20
import { uuid } from './uuid.js'
20
21
@@ -54,6 +55,8 @@ export const defaultOptions = {
54
55
httpStack : undefined ,
55
56
56
57
protocol : PROTOCOL_TUS_V1 as UploadOptions [ 'protocol' ] ,
58
+
59
+ stallDetection : undefined ,
57
60
}
58
61
59
62
export class BaseUpload {
@@ -105,6 +108,13 @@ export class BaseUpload {
105
108
// parts, if the parallelUploads option is used.
106
109
private _parallelUploadUrls ?: string [ ]
107
110
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
+
108
118
constructor ( file : UploadInput , options : UploadOptions ) {
109
119
// Warn about removed options from previous versions
110
120
if ( 'resume' in options ) {
@@ -121,6 +131,9 @@ export class BaseUpload {
121
131
this . options . chunkSize = Number ( this . options . chunkSize )
122
132
123
133
this . file = file
134
+
135
+ // Initialize stall detection options
136
+ this . options . stallDetection = this . _getStallDetectionDefaults ( options . stallDetection )
124
137
}
125
138
126
139
async findPreviousUploads ( ) : Promise < PreviousUpload [ ] > {
@@ -262,6 +275,9 @@ export class BaseUpload {
262
275
} else {
263
276
await this . _startSingleUpload ( )
264
277
}
278
+
279
+ // Setup stall detection
280
+ this . _setupStallDetection ( )
265
281
}
266
282
267
283
/**
@@ -457,6 +473,9 @@ export class BaseUpload {
457
473
// Set the aborted flag before any `await`s, so no new requests are started.
458
474
this . _aborted = true
459
475
476
+ // Clear any stall detection
477
+ this . _clearStallDetection ( )
478
+
460
479
// Stop any parallel partial uploads, that have been started in _startParallelUploads.
461
480
if ( this . _parallelUploads != null ) {
462
481
for ( const upload of this . _parallelUploads ) {
@@ -551,6 +570,12 @@ export class BaseUpload {
551
570
* @api private
552
571
*/
553
572
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
+
554
579
if ( typeof this . options . onProgress === 'function' ) {
555
580
this . options . onProgress ( bytesSent , bytesTotal )
556
581
}
@@ -985,6 +1010,137 @@ export class BaseUpload {
985
1010
_sendRequest ( req : HttpRequest , body ?: SliceType ) : Promise < HttpResponse > {
986
1011
return sendRequest ( req , body , this . options )
987
1012
}
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
+ }
988
1144
}
989
1145
990
1146
function encodeMetadata ( metadata : Record < string , string > ) : string {
0 commit comments