@@ -116,6 +116,15 @@ const NAVBAR_NOTIFICATION_ADDED_SUB = graphql`
116116 }
117117` ;
118118
119+ const XP_CACHE_TTL_MS = 30000 ;
120+ const XP_POLL_INTERVAL_MS = 30000 ;
121+ type XpLevelInfo = {
122+ level : number ;
123+ xpInLevel : number ;
124+ xpRequiredForLevelUp : number ;
125+ } ;
126+ const xpLevelCache = new Map < string , { value : XpLevelInfo ; timestamp : number } > ( ) ;
127+
119128/** ---------------- Utilities ---------------- */
120129function useIsTutor ( _frag : NavbarIsTutor$key ) {
121130 const data = useFragment (
@@ -558,16 +567,27 @@ function UserInfo({ tutor, userId }: { tutor: boolean; userId: string }) {
558567 ) ;
559568
560569 // XP/Level (keeps your HEAD logic)
561- const [ levelInfo , setLevelInfo ] = useState < {
562- level : number ;
563- xpInLevel : number ;
564- xpRequiredForLevelUp : number ;
565- } | null > ( null ) ;
570+ const [ levelInfo , setLevelInfo ] = useState < XpLevelInfo | null > ( null ) ;
571+ const xpFetchInFlightRef = useRef ( false ) ;
572+ const xpLastFetchAtRef = useRef ( 0 ) ;
566573
567574 // central XP fetcher (Relay)
568575 const relayEnv = useRelayEnvironment ( ) ;
569- const fetchXP = useCallback ( async ( ) => {
576+ const fetchXP = useCallback ( async ( force = false ) => {
570577 if ( ! userId ) return ;
578+
579+ const now = Date . now ( ) ;
580+ const cached = xpLevelCache . get ( userId ) ;
581+ if ( ! force && cached && now - cached . timestamp < XP_CACHE_TTL_MS ) {
582+ setLevelInfo ( cached . value ) ;
583+ return ;
584+ }
585+ if ( ! force && xpFetchInFlightRef . current ) return ;
586+ if ( ! force && now - xpLastFetchAtRef . current < 1500 ) return ;
587+
588+ xpFetchInFlightRef . current = true ;
589+ xpLastFetchAtRef . current = now ;
590+
571591 try {
572592 const query = graphql `
573593 query NavbarGetUserXPQuery($userID: ID!) {
@@ -592,52 +612,48 @@ function UserInfo({ tutor, userId }: { tutor: boolean; userId: string }) {
592612 : rawUser ?? null ;
593613
594614 if ( ! payload ) {
595- setLevelInfo ( { level : 0 , xpInLevel : 0 , xpRequiredForLevelUp : 1 } ) ;
615+ const fallback = { level : 0 , xpInLevel : 0 , xpRequiredForLevelUp : 1 } ;
616+ setLevelInfo ( fallback ) ;
617+ xpLevelCache . set ( userId , { value : fallback , timestamp : Date . now ( ) } ) ;
596618 return ;
597619 }
598620 const requiredXP = Number ( payload . requiredXP ?? 0 ) ;
599621 const exceedingXP = Number ( payload . exceedingXP ?? 0 ) ;
600622 const level = Number ( payload . level ?? 0 ) ;
601- setLevelInfo ( {
623+ const nextLevelInfo = {
602624 level : Number . isFinite ( level ) ? level : 0 ,
603625 xpInLevel : Number . isFinite ( exceedingXP ) ? exceedingXP : 0 ,
604626 xpRequiredForLevelUp :
605627 Number . isFinite ( requiredXP ) && requiredXP > 0 ? requiredXP : 1 ,
606- } ) ;
628+ } ;
629+ setLevelInfo ( nextLevelInfo ) ;
630+ xpLevelCache . set ( userId , { value : nextLevelInfo , timestamp : Date . now ( ) } ) ;
607631 } catch ( e ) {
608632 console . error ( "[Navbar XP] fetch failed" , e ) ;
609633 setLevelInfo ( { level : 0 , xpInLevel : 0 , xpRequiredForLevelUp : 1 } ) ;
634+ } finally {
635+ xpFetchInFlightRef . current = false ;
610636 }
611637 } , [ relayEnv , userId ] ) ;
612638
613639 // initial fetch and on identity changes
614640 useEffect ( ( ) => {
641+ if ( ! userId ) return ;
642+ const cached = xpLevelCache . get ( userId ) ;
643+ if ( cached ) {
644+ setLevelInfo ( cached . value ) ;
645+ }
615646 fetchXP ( ) ;
616647 } , [ fetchXP ] ) ;
617648
618- // refresh when window regains focus / becomes visible / custom XP events fire
649+ // periodic refresh only (no focus/visibility/ custom event triggers)
619650 useEffect ( ( ) => {
620- const handleFocus = ( ) => fetchXP ( ) ;
621- const handleVisible = ( ) => {
622- if ( document . visibilityState === "visible" ) fetchXP ( ) ;
623- } ;
624- const handleCustom = ( ) => fetchXP ( ) ; // dispatch window.dispatchEvent(new Event('xp:updated')) elsewhere
625-
626- window . addEventListener ( "focus" , handleFocus ) ;
627- document . addEventListener ( "visibilitychange" , handleVisible ) ;
628- window . addEventListener ( "xp:updated" , handleCustom as EventListener ) ;
629- window . addEventListener (
630- "meitrex:xp-updated" ,
631- handleCustom as EventListener
632- ) ;
651+ const interval = window . setInterval ( ( ) => {
652+ fetchXP ( true ) ;
653+ } , XP_POLL_INTERVAL_MS ) ;
654+
633655 return ( ) => {
634- window . removeEventListener ( "focus" , handleFocus ) ;
635- document . removeEventListener ( "visibilitychange" , handleVisible ) ;
636- window . removeEventListener ( "xp:updated" , handleCustom as EventListener ) ;
637- window . removeEventListener (
638- "meitrex:xp-updated" ,
639- handleCustom as EventListener
640- ) ;
656+ window . clearInterval ( interval ) ;
641657 } ;
642658 } , [ fetchXP ] ) ;
643659
@@ -653,7 +669,7 @@ function UserInfo({ tutor, userId }: { tutor: boolean; userId: string }) {
653669
654670 if ( ( remaining <= 0 || perc >= 100 ) && xpRetryRef . current < 3 ) {
655671 xpRetryRef . current += 1 ;
656- const t = setTimeout ( ( ) => fetchXP ( ) , 1200 ) ;
672+ const t = setTimeout ( ( ) => fetchXP ( true ) , 1200 ) ;
657673 return ( ) => clearTimeout ( t ) ;
658674 }
659675 // reset retries once things look normal
0 commit comments