1- import { useCallback , useEffect , useState } from 'react' ;
1+ import { useCallback , useEffect , useRef , useState } from 'react' ;
22import { logger } from '../lib/logger' ;
33
44type TabSession = {
@@ -42,107 +42,88 @@ export function useTabTrackingData(): UseTabTrackingDataReturn {
4242 const [ isLoading , setIsLoading ] = useState ( true ) ;
4343 const [ error , setError ] = useState < string | null > ( null ) ;
4444
45+ const isLoadingRef = useRef ( false ) ;
46+ const hasLoadedOnceRef = useRef ( false ) ;
47+ const intervalRef = useRef < number | null > ( null ) ;
48+
49+ // Wrapper that times out a message call and logs a per-call warning without throwing
50+ const callWithTimeout = useCallback (
51+ async < T > ( action : string , timeoutMs = 3000 ) : Promise < T | null > => {
52+ try {
53+ const responsePromise = browser . runtime . sendMessage ( { action } ) as Promise < T > ;
54+ const timeoutPromise = new Promise < null > ( ( resolve ) => {
55+ setTimeout ( ( ) => resolve ( null ) , timeoutMs ) ;
56+ } ) ;
57+ const result = ( await Promise . race ( [ responsePromise , timeoutPromise ] ) ) as T | null ;
58+ if ( result === null ) {
59+ logger . warn (
60+ `Popup data: failed to load ${ action . toLowerCase ( ) . replace ( 'get_' , '' ) . replace ( / _ / g, ' ' ) } ` ,
61+ {
62+ component : 'useTabTrackingData' ,
63+ reason : `Timed out: ${ action } ` ,
64+ }
65+ ) ;
66+ return null ;
67+ }
68+ return result ;
69+ } catch ( err ) {
70+ logger . warn (
71+ `Popup data: failed to load ${ action . toLowerCase ( ) . replace ( 'get_' , '' ) . replace ( / _ / g, ' ' ) } ` ,
72+ {
73+ component : 'useTabTrackingData' ,
74+ reason : err instanceof Error ? err . message : 'Unknown error' ,
75+ }
76+ ) ;
77+ return null ;
78+ }
79+ } ,
80+ [ ]
81+ ) ;
82+
4583 const loadData = useCallback ( async ( ) => {
84+ if ( isLoadingRef . current ) {
85+ // Prevent overlapping loads which caused duplicate timers and flicker
86+ return ;
87+ }
88+ isLoadingRef . current = true ;
89+
90+ // Only time when not already timing this logical load
4691 logger . debug ( 'Loading popup data' , { component : 'useTabTrackingData' } ) ;
4792 logger . time ( 'TabTrackingData Load' ) ;
48-
49- const withTimeout = < T > ( promise : Promise < T > , ms : number , label : string ) : Promise < T > => {
50- return new Promise < T > ( ( resolve , reject ) => {
51- const timer = setTimeout ( ( ) => {
52- reject ( new Error ( `Timed out: ${ label } ` ) ) ;
53- } , ms ) ;
54- promise . then (
55- ( value ) => {
56- clearTimeout ( timer ) ;
57- resolve ( value ) ;
58- } ,
59- ( reason ) => {
60- clearTimeout ( timer ) ;
61- reject ( reason ) ;
62- }
63- ) ;
64- } ) ;
65- } ;
93+
6694
6795 try {
68- setIsLoading ( true ) ;
96+ setIsLoading ( ! hasLoadedOnceRef . current ) ;
6997 setError ( null ) ;
7098
71- const results = await Promise . allSettled ( [
72- withTimeout (
73- browser . runtime . sendMessage ( { action : 'GET_DAILY_STATS' } ) as Promise < DailyStats | null > ,
74- 3000 ,
75- 'GET_DAILY_STATS'
76- ) ,
77- withTimeout (
78- browser . runtime . sendMessage ( {
79- action : 'GET_CURRENT_SESSION' ,
80- } ) as Promise < TabSession | null > ,
81- 3000 ,
82- 'GET_CURRENT_SESSION'
83- ) ,
84- withTimeout (
85- browser . runtime . sendMessage ( { action : 'GET_TRACKING_STATUS' } ) as Promise < boolean > ,
86- 3000 ,
87- 'GET_TRACKING_STATUS'
88- ) ,
89- withTimeout (
90- browser . runtime . sendMessage ( { action : 'GET_SYNC_STATUS' } ) as Promise < SyncStatus > ,
91- 3000 ,
92- 'GET_SYNC_STATUS'
93- ) ,
99+ const [ statsResponse , sessionResponse , trackingResponse , syncResponse ] = await Promise . all ( [
100+ callWithTimeout < DailyStats | null > ( 'GET_DAILY_STATS' ) ,
101+ callWithTimeout < TabSession | null > ( 'GET_CURRENT_SESSION' ) ,
102+ callWithTimeout < boolean > ( 'GET_TRACKING_STATUS' ) ,
103+ callWithTimeout < SyncStatus > ( 'GET_SYNC_STATUS' ) ,
94104 ] ) ;
95105
96- const [ statsRes , sessionRes , trackingRes , syncRes ] = results ;
97-
98- if ( statsRes . status === 'fulfilled' ) {
99- setDailyStats ( statsRes . value ) ;
100- } else {
101- logger . warn ( 'Popup data: failed to load daily stats' , {
102- component : 'useTabTrackingData' ,
103- reason : ( statsRes . reason as Error ) ?. message ,
104- } ) ;
106+ if ( statsResponse !== null ) {
107+ setDailyStats ( statsResponse ) ;
105108 }
106-
107- if ( sessionRes . status === 'fulfilled' ) {
108- setCurrentSession ( sessionRes . value ) ;
109- } else {
110- logger . warn ( 'Popup data: failed to load current session' , {
111- component : 'useTabTrackingData' ,
112- reason : ( sessionRes . reason as Error ) ?. message ,
113- } ) ;
109+ if ( sessionResponse !== null ) {
110+ setCurrentSession ( sessionResponse ) ;
114111 }
115-
116- if ( trackingRes . status === 'fulfilled' ) {
117- setIsTrackingEnabled ( trackingRes . value ) ;
118- } else {
119- logger . warn ( 'Popup data: failed to load tracking status' , {
120- component : 'useTabTrackingData' ,
121- reason : ( trackingRes . reason as Error ) ?. message ,
122- } ) ;
112+ if ( typeof trackingResponse === 'boolean' ) {
113+ setIsTrackingEnabled ( trackingResponse ) ;
123114 }
124-
125- if ( syncRes . status === 'fulfilled' ) {
126- setSyncStatus ( syncRes . value ) ;
127- } else {
128- logger . warn ( 'Popup data: failed to load sync status' , {
129- component : 'useTabTrackingData' ,
130- reason : ( syncRes . reason as Error ) ?. message ,
131- } ) ;
115+ if ( syncResponse !== null ) {
116+ setSyncStatus ( syncResponse ) ;
132117 }
133118
134119 logger . timeEnd ( 'TabTrackingData Load' ) ;
135120 logger . info ( 'Tab tracking data loaded (partial ok)' , {
136121 component : 'useTabTrackingData' ,
137- hasStats : statsRes . status === 'fulfilled' && ! ! statsRes . value ,
138- hasSession : sessionRes . status === 'fulfilled' && ! ! sessionRes . value ,
139- trackingEnabled :
140- trackingRes . status === 'fulfilled' ? Boolean ( trackingRes . value ) : undefined ,
122+ hasStats : ! ! statsResponse ,
123+ hasSession : ! ! sessionResponse ,
124+ trackingEnabled : ( trackingResponse as unknown as boolean ) ?? undefined ,
141125 } ) ;
142126
143- if ( results . some ( ( r ) => r . status === 'rejected' ) ) {
144- setError ( 'Some data failed to load. Try again.' ) ;
145- }
146127 } catch ( err ) {
147128 logger . timeEnd ( 'TabTrackingData Load' ) ;
148129 logger . error ( 'Failed to load tab tracking data' , err as Error , {
@@ -151,18 +132,31 @@ export function useTabTrackingData(): UseTabTrackingDataReturn {
151132 setError ( 'Failed to load tracking data' ) ;
152133 } finally {
153134 setIsLoading ( false ) ;
135+ isLoadingRef . current = false ;
136+ hasLoadedOnceRef . current = true ;
154137 }
155- } , [ ] ) ;
138+ } , [ callWithTimeout ] ) ;
156139
157140 useEffect ( ( ) => {
158141 loadData ( ) ;
159142
160- const interval = setInterval ( ( ) => {
143+ // Clear existing interval if any (hot-reloads/StrictMode)
144+ if ( intervalRef . current ) {
145+ clearInterval ( intervalRef . current ) ;
146+ intervalRef . current = null ;
147+ }
148+
149+ intervalRef . current = setInterval ( ( ) => {
161150 logger . trace ( 'Auto-refreshing tab tracking data' , { component : 'useTabTrackingData' } ) ;
162151 loadData ( ) ;
163- } , 5000 ) ;
152+ } , 5000 ) as unknown as number ;
164153
165- return ( ) => clearInterval ( interval ) ;
154+ return ( ) => {
155+ if ( intervalRef . current ) {
156+ clearInterval ( intervalRef . current ) ;
157+ intervalRef . current = null ;
158+ }
159+ } ;
166160 } , [ loadData ] ) ;
167161
168162 return {
0 commit comments