@@ -2171,11 +2171,74 @@ <h3>Why calculate PR costs?</h3>
21712171 }
21722172 } ) ;
21732173
2174- async function handleStreamingRequest ( endpoint , request , resultDiv ) {
2174+ async function handleStreamingRequest ( endpoint , request , resultDiv , maxRetries = 8 ) {
2175+ // Wrap streaming with automatic retry logic using exponential backoff with jitter
2176+ // Max retries = 8 allows for backoff up to 120s: 1s, 2s, 4s, 8s, 16s, 32s, 64s, 120s
2177+ for ( let attempt = 1 ; attempt <= maxRetries ; attempt ++ ) {
2178+ try {
2179+ await attemptStreamingRequest ( endpoint , request , resultDiv , attempt , maxRetries ) ;
2180+ if ( attempt > 1 ) {
2181+ console . log ( `Stream request succeeded after ${ attempt } attempts` ) ;
2182+ }
2183+ return ; // Success, exit
2184+ } catch ( error ) {
2185+ // Check if error is retryable (network/timeout errors)
2186+ const isRetryable = error . message . includes ( 'Failed to fetch' ) ||
2187+ error . message . includes ( 'network' ) ||
2188+ error . message . includes ( 'timeout' ) ||
2189+ error . message . includes ( 'aborted' ) ||
2190+ error . name === 'TypeError' ||
2191+ error . name === 'AbortError' ;
2192+
2193+ if ( isRetryable && attempt < maxRetries ) {
2194+ // Exponential backoff with jitter: 1s, 2s, 4s, 8s, 16s, 32s, 64s, up to 120s
2195+ const baseDelay = Math . min ( 1000 * Math . pow ( 2 , attempt - 1 ) , 120000 ) ;
2196+ // Add jitter: random value between 0% and 25% of base delay
2197+ const jitter = Math . random ( ) * 0.25 * baseDelay ;
2198+ const delay = Math . floor ( baseDelay + jitter ) ;
2199+
2200+ console . log ( `Stream connection lost (attempt ${ attempt } /${ maxRetries } ): ${ error . message } ` ) ;
2201+ console . log ( `Retrying in ${ delay } ms with exponential backoff + jitter` ) ;
2202+
2203+ // Show retry message to user
2204+ const submitBtn = document . querySelector ( 'button[type="submit"]' ) ;
2205+ submitBtn . textContent = `Connection lost, retrying in ${ Math . ceil ( delay / 1000 ) } s...` ;
2206+
2207+ await new Promise ( resolve => setTimeout ( resolve , delay ) ) ;
2208+ continue ;
2209+ }
2210+
2211+ // Non-retryable error or max retries exceeded
2212+ console . error ( `Stream request failed permanently: ${ error . message } ` ) ;
2213+ throw error ;
2214+ }
2215+ }
2216+ }
2217+
2218+ async function attemptStreamingRequest ( endpoint , request , resultDiv , attempt , maxRetries ) {
21752219 // EventSource doesn't support POST, so we need a different approach
21762220 // We'll use fetch to initiate, but handle it as a proper SSE stream
21772221 return new Promise ( ( resolve , reject ) => {
21782222 let progressContainer ;
2223+ let lastActivityTime = Date . now ( ) ;
2224+ let timeoutId ;
2225+ let reader ; // Declare reader in outer scope to prevent race condition
2226+
2227+ // Set up activity timeout (10 seconds of no data = connection lost)
2228+ // Server sends updates every ~5s, so 10s allows for network latency
2229+ const resetTimeout = ( ) => {
2230+ if ( timeoutId ) clearTimeout ( timeoutId ) ;
2231+ lastActivityTime = Date . now ( ) ;
2232+ timeoutId = setTimeout ( ( ) => {
2233+ const elapsed = Date . now ( ) - lastActivityTime ;
2234+ if ( elapsed >= 10000 ) {
2235+ if ( reader ) {
2236+ reader . cancel ( ) . catch ( ( ) => { } ) ; // Ignore cancel errors
2237+ }
2238+ reject ( new Error ( 'Stream timeout: no data received for 10 seconds' ) ) ;
2239+ }
2240+ } , 10000 ) ;
2241+ } ;
21792242
21802243 // Make the POST request with fetch
21812244 fetch ( endpoint , {
@@ -2186,23 +2249,29 @@ <h3>Why calculate PR costs?</h3>
21862249 body : JSON . stringify ( request )
21872250 } ) . then ( response => {
21882251 if ( ! response . ok ) {
2252+ if ( timeoutId ) clearTimeout ( timeoutId ) ;
21892253 return response . text ( ) . then ( error => {
21902254 throw new Error ( error || `HTTP ${ response . status } ` ) ;
21912255 } ) ;
21922256 }
21932257
21942258 // Use the proper streaming API with ReadableStream
2195- const reader = response . body . getReader ( ) ;
2259+ reader = response . body . getReader ( ) ;
21962260 const decoder = new TextDecoder ( ) ;
21972261 let buffer = '' ;
21982262
2263+ resetTimeout ( ) ; // Start timeout monitoring
2264+
21992265 function read ( ) {
22002266 reader . read ( ) . then ( ( { done, value} ) => {
22012267 if ( done ) {
2268+ if ( timeoutId ) clearTimeout ( timeoutId ) ;
22022269 resolve ( ) ;
22032270 return ;
22042271 }
22052272
2273+ resetTimeout ( ) ; // Reset timeout on data received
2274+
22062275 buffer += decoder . decode ( value , { stream : true } ) ;
22072276 const lines = buffer . split ( '\n' ) ;
22082277 buffer = lines . pop ( ) || '' ;
@@ -2215,7 +2284,8 @@ <h3>Why calculate PR costs?</h3>
22152284
22162285 if ( data . type === 'error' && ! data . pr ) {
22172286 // Global error
2218- reader . cancel ( ) ;
2287+ if ( timeoutId ) clearTimeout ( timeoutId ) ;
2288+ reader . cancel ( ) . catch ( ( ) => { } ) ; // Ignore cancel errors
22192289 reject ( new Error ( data . error ) ) ;
22202290 return ;
22212291 }
@@ -2365,13 +2435,15 @@ <h3>Why calculate PR costs?</h3>
23652435 // Continue reading
23662436 read ( ) ;
23672437 } ) . catch ( error => {
2438+ if ( timeoutId ) clearTimeout ( timeoutId ) ;
23682439 reject ( error ) ;
23692440 } ) ;
23702441 }
23712442
23722443 // Start reading
23732444 read ( ) ;
23742445 } ) . catch ( error => {
2446+ if ( timeoutId ) clearTimeout ( timeoutId ) ;
23752447 reject ( error ) ;
23762448 } ) ;
23772449 } ) ;
0 commit comments