@@ -46,11 +46,14 @@ let _notifGateDisabled = false; // Disabled after 403 (notifications scope not g
4646
4747/** PRs with pending/null check status: maps GraphQL node ID → databaseId */
4848const _hotPRs = new Map < string , number > ( ) ;
49+ /** Inverse index for O(1) eviction: maps databaseId → nodeId */
50+ const _hotPRsByDbId = new Map < number , string > ( ) ;
4951const MAX_HOT_PRS = 200 ;
5052
5153/** In-progress workflow runs: maps run ID → repo descriptor */
5254const _hotRuns = new Map < number , { owner : string ; repo : string } > ( ) ;
5355const MAX_HOT_RUNS = 30 ;
56+ const HOT_RUNS_CONCURRENCY = 10 ;
5457
5558/** Incremented each time rebuildHotSets() is called (full refresh completed).
5659 * Allows hot poll callbacks to detect stale results that overlap with a fresh
@@ -64,6 +67,7 @@ export function getHotPollGeneration(): number {
6467
6568export function clearHotSets ( ) : void {
6669 _hotPRs . clear ( ) ;
70+ _hotPRsByDbId . clear ( ) ;
6771 _hotRuns . clear ( ) ;
6872}
6973
@@ -72,6 +76,7 @@ export function resetPollState(): void {
7276 _lastSuccessfulFetch = null ;
7377 _notifGateDisabled = false ;
7478 _hotPRs . clear ( ) ;
79+ _hotPRsByDbId . clear ( ) ;
7580 _hotRuns . clear ( ) ;
7681 _hotPollGeneration = 0 ;
7782 _resetNotificationState ( ) ;
@@ -391,6 +396,7 @@ export function createPollCoordinator(
391396export function rebuildHotSets ( data : DashboardData ) : void {
392397 _hotPollGeneration ++ ;
393398 _hotPRs . clear ( ) ;
399+ _hotPRsByDbId . clear ( ) ;
394400 _hotRuns . clear ( ) ;
395401
396402 for ( const pr of data . pullRequests ) {
@@ -400,6 +406,7 @@ export function rebuildHotSets(data: DashboardData): void {
400406 break ;
401407 }
402408 _hotPRs . set ( pr . nodeId , pr . id ) ;
409+ _hotPRsByDbId . set ( pr . id , pr . nodeId ) ;
403410 }
404411 }
405412
@@ -459,9 +466,9 @@ export async function fetchHotData(): Promise<{
459466 // Workflow run fetches — bounded concurrency via pooledAllSettled
460467 const runEntries = [ ..._hotRuns . entries ( ) ] ;
461468 const runTasks = runEntries . map (
462- ( entry ) => async ( ) => fetchWorkflowRunById ( octokit , { id : entry [ 0 ] , ...entry [ 1 ] } )
469+ ( [ runId , descriptor ] ) => async ( ) => fetchWorkflowRunById ( octokit , { id : runId , ...descriptor } )
463470 ) ;
464- const runResults = await pooledAllSettled ( runTasks , 10 ) ;
471+ const runResults = await pooledAllSettled ( runTasks , HOT_RUNS_CONCURRENCY ) ;
465472 for ( const result of runResults ) {
466473 if ( result . status === "fulfilled" ) {
467474 runUpdates . set ( result . value . id , result . value ) ;
@@ -474,18 +481,17 @@ export async function fetchHotData(): Promise<{
474481 // The freshly rebuilt sets are authoritative — evicting from them based on
475482 // stale fetch results would corrupt the new data.
476483 if ( generation === _hotPollGeneration ) {
477- // Evict settled PRs
484+ // Evict settled PRs using inverse index for O(1) lookup
478485 for ( const [ databaseId , upd ] of prUpdates ) {
479486 if (
480487 upd . state === "CLOSED" ||
481488 upd . state === "MERGED" ||
482489 ( upd . checkStatus !== "pending" && upd . checkStatus !== null )
483490 ) {
484- for ( const [ nodeId , id ] of _hotPRs ) {
485- if ( id === databaseId ) {
486- _hotPRs . delete ( nodeId ) ;
487- break ;
488- }
491+ const nodeId = _hotPRsByDbId . get ( databaseId ) ;
492+ if ( nodeId ) {
493+ _hotPRs . delete ( nodeId ) ;
494+ _hotPRsByDbId . delete ( databaseId ) ;
489495 }
490496 }
491497 }
@@ -525,6 +531,7 @@ export function createHotPollCoordinator(
525531 const MAX_BACKOFF_MULTIPLIER = 8 ; // caps at 8× the base interval
526532
527533 function destroy ( ) : void {
534+ // Invalidates any in-flight cycle(); createEffect captures the new value as the next chain's seed
528535 chainGeneration ++ ;
529536 consecutiveFailures = 0 ;
530537 if ( timeoutId !== null ) {
@@ -548,6 +555,12 @@ export function createHotPollCoordinator(
548555 return ;
549556 }
550557
558+ // Skip fetch when no authenticated client (e.g., mid-logout)
559+ if ( ! getClient ( ) ) {
560+ schedule ( myGeneration ) ;
561+ return ;
562+ }
563+
551564 try {
552565 const { prUpdates, runUpdates, generation, hadErrors } = await fetchHotData ( ) ;
553566 if ( myGeneration !== chainGeneration ) return ; // Chain destroyed during fetch
@@ -556,10 +569,13 @@ export function createHotPollCoordinator(
556569 } else {
557570 consecutiveFailures = 0 ;
558571 }
559- onHotData ( prUpdates , runUpdates , generation ) ;
572+ if ( prUpdates . size > 0 || runUpdates . size > 0 ) {
573+ onHotData ( prUpdates , runUpdates , generation ) ;
574+ }
560575 } catch ( err ) {
561576 consecutiveFailures ++ ;
562- console . warn ( `[hot-poll] cycle failed (${ consecutiveFailures } x):` , err ) ;
577+ const message = err instanceof Error ? err . message : "Unknown hot-poll error" ;
578+ pushError ( "hot-poll" , message , true ) ;
563579 }
564580
565581 schedule ( myGeneration ) ;
0 commit comments