@@ -110,7 +110,24 @@ function getOptionalStringField(source: unknown, key: string): string | undefine
110110 return typeof value === 'string' ? value : undefined ;
111111}
112112
113- const ACCEPTED_QUEUE_REFRESH_DELAYS_MS = [ 1000 , 5000 , 15000 ] as const ;
113+ const ACCEPTED_QUEUE_POLL_INTERVAL_MS = 3000 ;
114+ const ACCEPTED_QUEUE_POLL_TIMEOUT_MS = 18000 ;
115+
116+ function listFindingsDataHasActiveAnalysis ( data : unknown ) : boolean {
117+ if ( typeof data !== 'object' || data === null ) return false ;
118+
119+ const runningCount = Reflect . get ( data , 'runningCount' ) ;
120+ if ( typeof runningCount === 'number' && runningCount > 0 ) return true ;
121+
122+ const findings = Reflect . get ( data , 'findings' ) ;
123+ if ( ! Array . isArray ( findings ) ) return false ;
124+
125+ return findings . some ( finding => {
126+ if ( typeof finding !== 'object' || finding === null ) return false ;
127+ const analysisStatus = Reflect . get ( finding , 'analysis_status' ) ;
128+ return analysisStatus === 'pending' || analysisStatus === 'running' ;
129+ } ) ;
130+ }
114131
115132type SecurityAgentProviderProps = {
116133 organizationId ?: string ;
@@ -125,30 +142,96 @@ export function SecurityAgentProvider({ organizationId, children }: SecurityAgen
125142 const [ startingAnalysisIds , setStartingAnalysisIds ] = useState < Set < string > > ( new Set ( ) ) ;
126143 const [ gitHubError , setGitHubError ] = useState < string | null > ( null ) ;
127144 const toggleEnabledInFlightRef = useRef ( false ) ;
128- const acceptedQueueRefreshTimersRef = useRef < number [ ] > ( [ ] ) ;
145+ const acceptedQueuePollRef = useRef < { intervalId : number ; timeoutId : number } | null > ( null ) ;
146+
147+ const clearAcceptedQueuePoll = useCallback ( ( ) => {
148+ const activePoll = acceptedQueuePollRef . current ;
149+ if ( ! activePoll ) return ;
150+ window . clearInterval ( activePoll . intervalId ) ;
151+ window . clearTimeout ( activePoll . timeoutId ) ;
152+ acceptedQueuePollRef . current = null ;
153+ } , [ ] ) ;
154+
155+ useEffect ( ( ) => clearAcceptedQueuePoll , [ clearAcceptedQueuePoll ] ) ;
156+
157+ const invalidateAcceptedQueueQueries = useCallback ( ( ) => {
158+ if ( isOrg && organizationId ) {
159+ const ownerInput = { organizationId } ;
160+ void Promise . all ( [
161+ queryClient . invalidateQueries ( {
162+ queryKey : trpc . organizations . securityAgent . listFindings . queryKey ( ownerInput ) ,
163+ } ) ,
164+ queryClient . invalidateQueries ( {
165+ queryKey : trpc . organizations . securityAgent . getFinding . queryKey ( ownerInput ) ,
166+ } ) ,
167+ queryClient . invalidateQueries ( {
168+ queryKey : trpc . organizations . securityAgent . getAnalysis . queryKey ( ownerInput ) ,
169+ } ) ,
170+ queryClient . invalidateQueries ( {
171+ queryKey : trpc . organizations . securityAgent . getStats . queryKey ( ownerInput ) ,
172+ } ) ,
173+ queryClient . invalidateQueries ( {
174+ queryKey : trpc . organizations . securityAgent . getDashboardStats . queryKey ( ownerInput ) ,
175+ } ) ,
176+ queryClient . invalidateQueries ( {
177+ queryKey : trpc . organizations . securityAgent . getLastSyncTime . queryKey ( ownerInput ) ,
178+ } ) ,
179+ queryClient . invalidateQueries ( {
180+ queryKey : trpc . organizations . securityAgent . getRepositories . queryKey ( ownerInput ) ,
181+ } ) ,
182+ queryClient . invalidateQueries ( {
183+ queryKey : trpc . organizations . securityAgent . getOrphanedRepositories . queryKey ( ownerInput ) ,
184+ } ) ,
185+ queryClient . invalidateQueries ( {
186+ queryKey : trpc . organizations . securityAgent . getAutoDismissEligible . queryKey ( ownerInput ) ,
187+ } ) ,
188+ ] ) ;
189+ return ;
190+ }
129191
130- useEffect (
131- ( ) => ( ) => {
132- for ( const timer of acceptedQueueRefreshTimersRef . current ) {
133- window . clearTimeout ( timer ) ;
192+ void Promise . all ( [
193+ queryClient . invalidateQueries ( { queryKey : trpc . securityAgent . listFindings . queryKey ( ) } ) ,
194+ queryClient . invalidateQueries ( { queryKey : trpc . securityAgent . getFinding . queryKey ( ) } ) ,
195+ queryClient . invalidateQueries ( { queryKey : trpc . securityAgent . getAnalysis . queryKey ( ) } ) ,
196+ queryClient . invalidateQueries ( { queryKey : trpc . securityAgent . getStats . queryKey ( ) } ) ,
197+ queryClient . invalidateQueries ( { queryKey : trpc . securityAgent . getDashboardStats . queryKey ( ) } ) ,
198+ queryClient . invalidateQueries ( { queryKey : trpc . securityAgent . getLastSyncTime . queryKey ( ) } ) ,
199+ queryClient . invalidateQueries ( { queryKey : trpc . securityAgent . getRepositories . queryKey ( ) } ) ,
200+ queryClient . invalidateQueries ( {
201+ queryKey : trpc . securityAgent . getOrphanedRepositories . queryKey ( ) ,
202+ } ) ,
203+ queryClient . invalidateQueries ( {
204+ queryKey : trpc . securityAgent . getAutoDismissEligible . queryKey ( ) ,
205+ } ) ,
206+ ] ) ;
207+ } , [ isOrg , organizationId , queryClient , trpc ] ) ;
208+
209+ const cachedListFindingsHasActiveAnalysis = useCallback ( ( ) => {
210+ const queryKey = isOrg
211+ ? trpc . organizations . securityAgent . listFindings . queryKey (
212+ organizationId ? { organizationId } : undefined
213+ )
214+ : trpc . securityAgent . listFindings . queryKey ( ) ;
215+
216+ return queryClient
217+ . getQueriesData ( { queryKey } )
218+ . some ( ( [ , data ] ) => listFindingsDataHasActiveAnalysis ( data ) ) ;
219+ } , [ isOrg , organizationId , queryClient , trpc ] ) ;
220+
221+ const pollAcceptedQueueMutation = useCallback ( ( ) => {
222+ clearAcceptedQueuePoll ( ) ;
223+ invalidateAcceptedQueueQueries ( ) ;
224+
225+ const intervalId = window . setInterval ( ( ) => {
226+ invalidateAcceptedQueueQueries ( ) ;
227+ if ( cachedListFindingsHasActiveAnalysis ( ) ) {
228+ clearAcceptedQueuePoll ( ) ;
134229 }
135- acceptedQueueRefreshTimersRef . current = [ ] ;
136- } ,
137- [ ]
138- ) ;
230+ } , ACCEPTED_QUEUE_POLL_INTERVAL_MS ) ;
139231
140- const refreshAcceptedQueueMutation = useCallback ( ( ) => {
141- void queryClient . invalidateQueries ( ) ;
142- for ( const delay of ACCEPTED_QUEUE_REFRESH_DELAYS_MS ) {
143- const timer = window . setTimeout ( ( ) => {
144- void queryClient . invalidateQueries ( ) ;
145- acceptedQueueRefreshTimersRef . current = acceptedQueueRefreshTimersRef . current . filter (
146- pendingTimer => pendingTimer !== timer
147- ) ;
148- } , delay ) ;
149- acceptedQueueRefreshTimersRef . current . push ( timer ) ;
150- }
151- } , [ queryClient ] ) ;
232+ const timeoutId = window . setTimeout ( clearAcceptedQueuePoll , ACCEPTED_QUEUE_POLL_TIMEOUT_MS ) ;
233+ acceptedQueuePollRef . current = { intervalId, timeoutId } ;
234+ } , [ cachedListFindingsHasActiveAnalysis , clearAcceptedQueuePoll , invalidateAcceptedQueueQueries ] ) ;
152235
153236 // Permission status query
154237 const { data : permissionData , isLoading : isLoadingPermission } = useQuery (
@@ -188,7 +271,7 @@ export function SecurityAgentProvider({ organizationId, children }: SecurityAgen
188271 onSuccess : ( ) => {
189272 setGitHubError ( null ) ;
190273 toast . success ( 'Sync queued' ) ;
191- refreshAcceptedQueueMutation ( ) ;
274+ pollAcceptedQueueMutation ( ) ;
192275 } ,
193276 onError : error => {
194277 const message = error instanceof Error ? error . message : String ( error ) ;
@@ -209,7 +292,7 @@ export function SecurityAgentProvider({ organizationId, children }: SecurityAgen
209292 trpc . organizations . securityAgent . dismissFinding . mutationOptions ( {
210293 onSuccess : ( ) => {
211294 toast . success ( 'Finding dismissed' ) ;
212- refreshAcceptedQueueMutation ( ) ;
295+ pollAcceptedQueueMutation ( ) ;
213296 } ,
214297 onError : error => {
215298 toast . error ( 'Failed to dismiss finding' , { description : error . message } ) ;
@@ -256,7 +339,7 @@ export function SecurityAgentProvider({ organizationId, children }: SecurityAgen
256339 onSuccess : async ( _data , variables ) => {
257340 setGitHubError ( null ) ;
258341 toast . success ( manualAnalysisAdmissionCopy . successTitle ) ;
259- refreshAcceptedQueueMutation ( ) ;
342+ pollAcceptedQueueMutation ( ) ;
260343 setStartingAnalysisIds ( prev => {
261344 const next = new Set ( prev ) ;
262345 next . delete ( variables . findingId ) ;
@@ -307,7 +390,7 @@ export function SecurityAgentProvider({ organizationId, children }: SecurityAgen
307390 onSuccess : ( ) => {
308391 setGitHubError ( null ) ;
309392 toast . success ( 'Sync queued' ) ;
310- refreshAcceptedQueueMutation ( ) ;
393+ pollAcceptedQueueMutation ( ) ;
311394 } ,
312395 onError : error => {
313396 const message = error instanceof Error ? error . message : String ( error ) ;
@@ -328,7 +411,7 @@ export function SecurityAgentProvider({ organizationId, children }: SecurityAgen
328411 trpc . securityAgent . dismissFinding . mutationOptions ( {
329412 onSuccess : ( ) => {
330413 toast . success ( 'Finding dismissed' ) ;
331- refreshAcceptedQueueMutation ( ) ;
414+ pollAcceptedQueueMutation ( ) ;
332415 } ,
333416 onError : error => {
334417 toast . error ( 'Failed to dismiss finding' , { description : error . message } ) ;
@@ -375,7 +458,7 @@ export function SecurityAgentProvider({ organizationId, children }: SecurityAgen
375458 onSuccess : async ( _data , variables ) => {
376459 setGitHubError ( null ) ;
377460 toast . success ( manualAnalysisAdmissionCopy . successTitle ) ;
378- refreshAcceptedQueueMutation ( ) ;
461+ pollAcceptedQueueMutation ( ) ;
379462 setStartingAnalysisIds ( prev => {
380463 const next = new Set ( prev ) ;
381464 next . delete ( variables . findingId ) ;
0 commit comments