@@ -50,6 +50,8 @@ const List: React.FC = () => {
5050 const { t } = useTranslation ( 'list' ) ;
5151 const { t : tCommon } = useTranslation ( 'common' ) ;
5252 const cloudContext = useContext ( CloudContext ) ;
53+ const cloudIsAuthenticated = cloudContext ?. isAuthenticated ?? false ;
54+ const cloudGetTasks = cloudContext ?. getTasks ;
5355
5456 const [ loading , setLoading ] = useState ( false ) ;
5557 const [ data , setData ] = useState < ( Task | CloudTask ) [ ] > ( [ ] ) ;
@@ -66,6 +68,12 @@ const List: React.FC = () => {
6668 const paginationRef = useRef ( pagination ) ;
6769 paginationRef . current = pagination ;
6870
71+ // Refs for debounced cloud SSE refresh
72+ const pendingRefreshRef = useRef ( false ) ;
73+ const refreshTimerRef = useRef < ReturnType < typeof setTimeout > | null > ( null ) ;
74+ const dataRef = useRef ( data ) ;
75+ dataRef . current = data ;
76+
6977 // Max items to fetch for unified sorting and local pagination
7078 const MAX_FETCH_ITEMS = 100 ;
7179 const buildLocalModelValue = ( modelId : string , providerId : number ) => `${ modelId } @${ providerId } ` ;
@@ -127,8 +135,8 @@ const List: React.FC = () => {
127135 ] ;
128136
129137 // Only fetch cloud tasks if authenticated
130- if ( cloudContext ?. isAuthenticated ) {
131- promises . push ( cloudContext . getTasks ( 1 , MAX_FETCH_ITEMS ) ) ;
138+ if ( cloudIsAuthenticated && cloudGetTasks ) {
139+ promises . push ( cloudGetTasks ( 1 , MAX_FETCH_ITEMS ) ) ;
132140 }
133141
134142 const results = await Promise . all ( promises ) ;
@@ -240,7 +248,7 @@ const List: React.FC = () => {
240248 } finally {
241249 setLoading ( false ) ;
242250 }
243- } , [ message , t , cloudContext ] ) ;
251+ } , [ message , t , cloudIsAuthenticated , cloudGetTasks ] ) ;
244252
245253 const handleTaskEvent = useCallback ( ( event : any ) => {
246254 const { type, taskId, task } = event ;
@@ -306,36 +314,44 @@ const List: React.FC = () => {
306314
307315 console . log ( '[List] Registering cloud SSE event listener' ) ;
308316
309- // Track tasks not found in list to trigger a single refresh
310- let pendingRefresh = false ;
311-
312317 const handleCloudEvent = ( event : CloudSSEEvent ) => {
313- const { type, data } = event ;
318+ const { type, data : eventData } = event ;
314319
315320 // Skip non-business events
316321 if ( type === 'heartbeat' || type === 'connected' ) return ;
317322
318- const taskId = ( data as any ) . task_id ;
323+ const taskId = ( eventData as any ) . task_id ;
319324 if ( ! taskId ) return ;
320325
321326 console . log ( `[List] Cloud SSE event: type=${ type } , task_id=${ taskId } ` ) ;
322327
323- setData ( prevData => {
324- const index = prevData . findIndex ( t => t . id === taskId ) ;
325- if ( index === - 1 ) {
326- // Task not in list, schedule a refresh outside of setState
327- if ( ! pendingRefresh ) {
328- pendingRefresh = true ;
329- queueMicrotask ( ( ) => {
330- pendingRefresh = false ;
331- fetchTasks ( paginationRef . current . current , paginationRef . current . pageSize ) ;
332- } ) ;
333- }
334- return prevData ;
328+ // Check if task is in current list using ref (avoids scheduling inside setState)
329+ const currentData = dataRef . current ;
330+ const index = currentData . findIndex ( t => t . id === taskId ) ;
331+
332+ if ( index === - 1 ) {
333+ // Task not in list — debounced refresh (500ms merge window)
334+ if ( ! pendingRefreshRef . current ) {
335+ pendingRefreshRef . current = true ;
336+ refreshTimerRef . current = setTimeout ( async ( ) => {
337+ try {
338+ await fetchTasks ( paginationRef . current . current , paginationRef . current . pageSize ) ;
339+ } finally {
340+ pendingRefreshRef . current = false ;
341+ refreshTimerRef . current = null ;
342+ }
343+ } , 500 ) ;
335344 }
345+ return ;
346+ }
347+
348+ // Task found in list — apply incremental update
349+ setData ( prevData => {
350+ const idx = prevData . findIndex ( t => t . id === taskId ) ;
351+ if ( idx === - 1 ) return prevData ;
336352
337353 const newData = [ ...prevData ] ;
338- const task = { ...newData [ index ] } ;
354+ const task = { ...newData [ idx ] } ;
339355
340356 switch ( type ) {
341357 case 'page_started' :
@@ -344,16 +360,15 @@ const List: React.FC = () => {
344360 break ;
345361 }
346362 case 'page_completed' : {
347- const pageNumber = ( data as any ) . page ;
348- const totalPages = ( data as any ) . total_pages || task . pages || 1 ;
363+ const pageNumber = ( eventData as any ) . page ;
364+ const totalPages = ( eventData as any ) . total_pages || task . pages || 1 ;
349365
350366 if ( task . status === 8 ) {
351367 // Retry scenario (PARTIAL_FAILED): increment completed, decrement failed
352368 task . completed_count = ( task . completed_count || 0 ) + 1 ;
353369 task . failed_count = Math . max ( 0 , ( task . failed_count || 0 ) - 1 ) ;
354370 } else {
355371 // Normal processing: use page number as progress indicator
356- // page is 1-based, so completed_count = page number when pages complete in order
357372 task . completed_count = Math . max ( task . completed_count || 0 , pageNumber || 0 ) ;
358373 }
359374
@@ -362,20 +377,19 @@ const List: React.FC = () => {
362377 break ;
363378 }
364379 case 'page_failed' : {
365- // Increment as approximation; the 'completed' event provides authoritative pages_failed
366380 task . failed_count = ( task . failed_count || 0 ) + 1 ;
367381 break ;
368382 }
369383 case 'completed' : {
370- task . status = ( data as any ) . status || 6 ;
384+ task . status = ( eventData as any ) . status || 6 ;
371385 task . progress = 100 ;
372- task . completed_count = ( data as any ) . pages_completed ;
373- task . failed_count = ( data as any ) . pages_failed ;
386+ task . completed_count = ( eventData as any ) . pages_completed ;
387+ task . failed_count = ( eventData as any ) . pages_failed ;
374388 break ;
375389 }
376390 case 'error' : {
377391 task . status = 0 ; // FAILED
378- task . error = ( data as any ) . error ;
392+ task . error = ( eventData as any ) . error ;
379393 break ;
380394 }
381395 case 'cancelled' : {
@@ -384,14 +398,14 @@ const List: React.FC = () => {
384398 }
385399 case 'pdf_ready' : {
386400 task . status = 3 ; // PROCESSING (splitting done, pages ready for conversion)
387- task . pages = ( data as any ) . page_count ;
401+ task . pages = ( eventData as any ) . page_count ;
388402 break ;
389403 }
390404 default :
391405 return prevData ;
392406 }
393407
394- newData [ index ] = task ;
408+ newData [ idx ] = task ;
395409 // Sync module-level progress cache so it survives component remount
396410 if ( task . id ) {
397411 const terminalStatuses = [ 0 , 6 , 7 , 8 ] ; // FAILED, COMPLETED, CANCELLED, PARTIAL_FAILED
@@ -415,6 +429,12 @@ const List: React.FC = () => {
415429 return ( ) => {
416430 console . log ( '[List] Cleaning up cloud SSE event listener' ) ;
417431 cleanup ( ) ;
432+ // Clear any pending debounced refresh
433+ if ( refreshTimerRef . current ) {
434+ clearTimeout ( refreshTimerRef . current ) ;
435+ refreshTimerRef . current = null ;
436+ pendingRefreshRef . current = false ;
437+ }
418438 } ;
419439 } , [ fetchTasks ] ) ;
420440
0 commit comments