@@ -30,6 +30,12 @@ class InjectedScript {
3030 } ) ;
3131 private isInitialized = false ;
3232
33+ // Debounce timers for observer count changes (to handle virtualized lists)
34+ private queryObserverDebounceTimer : ReturnType < typeof setTimeout > | null =
35+ null ;
36+ private mutationObserverDebounceTimer : ReturnType < typeof setTimeout > | null =
37+ null ;
38+
3339 // Initialize injected script
3440 initialize ( ) : void {
3541 if ( this . isInitialized ) return ;
@@ -131,7 +137,37 @@ class InjectedScript {
131137 }
132138
133139 // Subscribe to cache changes
134- this . queryUnsubscribe = queryClient . getQueryCache ( ) . subscribe ( ( ) => {
140+ // Filter events carefully to prevent infinite loops while still tracking subscriber count
141+ this . queryUnsubscribe = queryClient . getQueryCache ( ) . subscribe ( ( event ) => {
142+ // Ignore observer result/option updates as these can cause infinite loops
143+ // when storage updates trigger React component re-renders
144+ if (
145+ event . type === "observerResultsUpdated" ||
146+ event . type === "observerOptionsUpdated"
147+ ) {
148+ return ;
149+ }
150+
151+ // For observer add/remove events (e.g., virtualized lists scrolling),
152+ // debounce to avoid hundreds of updates per second
153+ if (
154+ event . type === "observerAdded" ||
155+ event . type === "observerRemoved"
156+ ) {
157+ // Clear existing timer
158+ if ( this . queryObserverDebounceTimer ) {
159+ clearTimeout ( this . queryObserverDebounceTimer ) ;
160+ }
161+
162+ // Debounce: wait 150ms after last observer change before updating
163+ this . queryObserverDebounceTimer = setTimeout ( ( ) => {
164+ this . sendQueryDataUpdate ( ) ;
165+ this . queryObserverDebounceTimer = null ;
166+ } , 150 ) ;
167+ return ;
168+ }
169+
170+ // Send updates immediately for real cache changes: 'added', 'removed', 'updated'
135171 this . sendQueryDataUpdate ( ) ;
136172 } ) ;
137173 } catch ( error ) {
@@ -151,9 +187,35 @@ class InjectedScript {
151187 }
152188
153189 // Subscribe to mutation cache changes
190+ // Filter events carefully to prevent infinite loops while still tracking subscriber count
154191 this . mutationUnsubscribe = queryClient
155192 . getMutationCache ( )
156- . subscribe ( ( ) => {
193+ . subscribe ( ( event ) => {
194+ // Ignore observer option updates as these can cause infinite loops
195+ // when storage updates trigger React component re-renders
196+ if ( event . type === "observerOptionsUpdated" ) {
197+ return ;
198+ }
199+
200+ // For observer add/remove events, debounce to avoid excessive updates
201+ if (
202+ event . type === "observerAdded" ||
203+ event . type === "observerRemoved"
204+ ) {
205+ // Clear existing timer
206+ if ( this . mutationObserverDebounceTimer ) {
207+ clearTimeout ( this . mutationObserverDebounceTimer ) ;
208+ }
209+
210+ // Debounce: wait 150ms after last observer change before updating
211+ this . mutationObserverDebounceTimer = setTimeout ( ( ) => {
212+ this . sendMutationDataUpdate ( ) ;
213+ this . mutationObserverDebounceTimer = null ;
214+ } , 150 ) ;
215+ return ;
216+ }
217+
218+ // Send updates immediately for real cache changes: 'added', 'removed', 'updated'
157219 this . sendMutationDataUpdate ( ) ;
158220 } ) ;
159221 } catch ( error ) {
@@ -277,6 +339,17 @@ class InjectedScript {
277339
278340 // Cleanup subscriptions
279341 private cleanupSubscriptions ( ) : void {
342+ // Clear any pending debounce timers
343+ if ( this . queryObserverDebounceTimer ) {
344+ clearTimeout ( this . queryObserverDebounceTimer ) ;
345+ this . queryObserverDebounceTimer = null ;
346+ }
347+
348+ if ( this . mutationObserverDebounceTimer ) {
349+ clearTimeout ( this . mutationObserverDebounceTimer ) ;
350+ this . mutationObserverDebounceTimer = null ;
351+ }
352+
280353 if ( this . queryUnsubscribe ) {
281354 try {
282355 this . queryUnsubscribe ( ) ;
0 commit comments