@@ -74,13 +74,19 @@ export function useSandbox(isActive = false) {
7474 useEffect ( ( ) => { tabsRef . current = tabs ; } , [ tabs ] ) ;
7575
7676 const fetchTree = useCallback ( async ( ) => {
77+ const controller = new AbortController ( ) ;
78+ const timeout = setTimeout ( ( ) => controller . abort ( ) , 5000 ) ;
7779 try {
78- const res = await fetch ( `${ API_BASE } /sandbox/tree` ) ;
80+ const res = await fetch ( `${ API_BASE } /sandbox/tree` , {
81+ signal : controller . signal ,
82+ } ) ;
7983 if ( ! res . ok ) return ;
8084 const data = await res . json ( ) ;
8185 setTree ( data ) ;
8286 } catch {
83- // silently fail -- sandbox may not exist yet
87+ // silently fail -- sandbox may not exist yet, or request timed out
88+ } finally {
89+ clearTimeout ( timeout ) ;
8490 }
8591 } , [ ] ) ;
8692
@@ -371,8 +377,13 @@ export function useSandbox(isActive = false) {
371377 const name = tab . path . split ( "/" ) . pop ( ) ?? "" ;
372378 const cat = fileCategory ( name ) ;
373379 if ( cat === "image" || cat === "pdf" ) continue ;
380+ const controller = new AbortController ( ) ;
381+ const timeout = setTimeout ( ( ) => controller . abort ( ) , 5000 ) ;
374382 try {
375- const res = await fetch ( `${ API_BASE } /sandbox/file?path=${ encodeURIComponent ( tab . path ) } ` ) ;
383+ const res = await fetch (
384+ `${ API_BASE } /sandbox/file?path=${ encodeURIComponent ( tab . path ) } ` ,
385+ { signal : controller . signal } ,
386+ ) ;
376387 const content = res . ok
377388 ? await res . text ( )
378389 : `[Error: ${ res . status } ${ res . statusText } ]` ;
@@ -383,24 +394,36 @@ export function useSandbox(isActive = false) {
383394 } ) ;
384395 } catch {
385396 // silently skip tabs that fail to refresh
397+ } finally {
398+ clearTimeout ( timeout ) ;
386399 }
387400 }
388401 } , [ ] ) ;
389402
390403 useEffect ( ( ) => {
391- fetchTree ( ) ;
392- if ( isActive ) {
393- const interval = setInterval ( ( ) => {
394- fetchTree ( ) ;
395- refreshOpenTabs ( ) ;
396- } , 2000 ) ;
397- return ( ) => clearInterval ( interval ) ;
398- } else {
399- const interval = setInterval ( fetchTree , 3000 ) ;
400- return ( ) => clearInterval ( interval ) ;
401- }
404+ let cancelled = false ;
405+
406+ const poll = async ( ) => {
407+ if ( cancelled ) return ;
408+ await fetchTree ( ) ;
409+ if ( isActive && ! cancelled ) await refreshOpenTabs ( ) ;
410+ if ( ! cancelled ) {
411+ setTimeout ( poll , isActive ? 1500 : 3000 ) ;
412+ }
413+ } ;
414+
415+ poll ( ) ;
416+ return ( ) => { cancelled = true ; } ;
402417 } , [ isActive , fetchTree , refreshOpenTabs ] ) ;
403418
419+ useEffect ( ( ) => {
420+ const onVisible = ( ) => {
421+ if ( document . visibilityState === "visible" ) fetchTree ( ) ;
422+ } ;
423+ document . addEventListener ( "visibilitychange" , onVisible ) ;
424+ return ( ) => document . removeEventListener ( "visibilitychange" , onVisible ) ;
425+ } , [ fetchTree ] ) ;
426+
404427 return {
405428 tree,
406429 tabs,
0 commit comments