@@ -13,8 +13,14 @@ if (!supabaseUrl || !supabasePublishableKey) {
1313 } ) ;
1414}
1515
16- // Track rate limit status
16+ // Track rate limit status with exponential backoff
1717let rateLimitedUntil : number = 0 ;
18+ let rateLimitBackoff : number = 30000 ; // Start with 30 seconds
19+ let consecutiveRateLimits : number = 0 ;
20+
21+ // Cache for session to avoid excessive getSession calls
22+ let cachedSessionToken : string | null = null ;
23+ let cachedSessionExpiry : number = 0 ;
1824
1925// Export function to check rate limit status
2026export const isRateLimited = ( ) => rateLimitedUntil > Date . now ( ) ;
@@ -24,6 +30,9 @@ export const supabase = createClient(supabaseUrl, supabasePublishableKey, {
2430 persistSession : true ,
2531 autoRefreshToken : true ,
2632 detectSessionInUrl : true ,
33+ // Reduce refresh frequency - only refresh when token has 1 minute left (instead of default 60 seconds)
34+ // This helps prevent conflicts with our manual refresh logic
35+ autoRefreshTickDuration : 60 ,
2736 // Use the default storage key that matches the project
2837 // This should be 'sb-lnvjsqyvhczgxvygbqer-auth-token'
2938 // Let Supabase handle the key automatically
@@ -55,16 +64,62 @@ export const supabase = createClient(supabaseUrl, supabasePublishableKey, {
5564 // For all Supabase API calls, ensure we're using the latest session
5665 if ( typeof url === 'string' && ( url . includes ( '/rest/v1/' ) || url . includes ( '/functions/v1/' ) ) ) {
5766 try {
58- // Get the current session from auth state
59- const currentSession = await supabase . auth . getSession ( ) ;
60- if ( currentSession . data . session ?. access_token ) {
61- // Ensure the Authorization header is set with the current token
67+ let accessToken = null ;
68+
69+ // First check our in-memory cache (valid for 5 seconds to batch rapid API calls)
70+ const now = Date . now ( ) ;
71+ if ( cachedSessionToken && cachedSessionExpiry > now ) {
72+ accessToken = cachedSessionToken ;
73+ } else {
74+ // Cache expired, check localStorage next
75+ const storageKey = `sb-${ supabaseUrl . split ( '//' ) [ 1 ] . split ( '.' ) [ 0 ] } -auth-token` ;
76+ const storedSession = localStorage . getItem ( storageKey ) ;
77+
78+ if ( storedSession ) {
79+ try {
80+ const sessionData = JSON . parse ( storedSession ) ;
81+ if ( sessionData ?. access_token ) {
82+ // Check if token is not too expired (allow up to 2 hours expired for recovery)
83+ const payload = JSON . parse ( atob ( sessionData . access_token . split ( '.' ) [ 1 ] ) ) ;
84+ const tokenExp = payload . exp ;
85+ const nowSeconds = Math . floor ( now / 1000 ) ;
86+ const timeUntilExpiry = tokenExp - nowSeconds ;
87+
88+ if ( timeUntilExpiry > - 7200 ) { // Within 2 hours of expiry
89+ accessToken = sessionData . access_token ;
90+ // Cache it for 5 seconds to avoid repeated parsing
91+ cachedSessionToken = accessToken ;
92+ cachedSessionExpiry = now + 5000 ;
93+ }
94+ }
95+ } catch ( e ) {
96+ // Ignore parsing errors
97+ }
98+ }
99+
100+ // Only call getSession if we absolutely need to (no cached or stored token)
101+ // AND we're not rate limited
102+ if ( ! accessToken && ! isRateLimited ( ) ) {
103+ const currentSession = await supabase . auth . getSession ( ) ;
104+ if ( currentSession . data . session ?. access_token ) {
105+ accessToken = currentSession . data . session . access_token ;
106+ // Cache it for 5 seconds
107+ cachedSessionToken = accessToken ;
108+ cachedSessionExpiry = now + 5000 ;
109+ }
110+ }
111+ }
112+
113+ // Set the Authorization header if we have a token
114+ if ( accessToken ) {
62115 const headers = new Headers ( options . headers || { } ) ;
63- headers . set ( 'Authorization' , `Bearer ${ currentSession . data . session . access_token } ` ) ;
116+ headers . set ( 'Authorization' , `Bearer ${ accessToken } ` ) ;
117+ headers . set ( 'apikey' , supabasePublishableKey ) ; // Also ensure API key is set
64118 options = { ...options , headers } ;
65119 }
66120 } catch ( e ) {
67121 // If getting session fails, proceed with original options
122+ console . warn ( 'Failed to add auth headers:' , e ) ;
68123 }
69124 }
70125 // Check if this is a token refresh request
@@ -160,13 +215,21 @@ export const supabase = createClient(supabaseUrl, supabasePublishableKey, {
160215 // Check if the existing signal is already aborted
161216 if ( options . signal . aborted ) {
162217 clearTimeout ( timeoutId ) ;
163- console . log ( 'Skipping fetch - existing signal already aborted' ) ;
218+ // Don't log for routine aborts (component unmounts, etc)
219+ if ( ! ( window as any ) . __suppressAbortLogs ) {
220+ console . log ( 'Skipping fetch - existing signal already aborted' ) ;
221+ }
164222 throw new DOMException ( 'The user aborted a request.' , 'AbortError' ) ;
165223 }
166224 // Create a combined signal that aborts if either signal aborts
167225 const combinedController = new AbortController ( ) ;
168- options . signal . addEventListener ( 'abort' , ( ) => combinedController . abort ( ) ) ;
169- controller . signal . addEventListener ( 'abort' , ( ) => combinedController . abort ( ) ) ;
226+ const abortHandler = ( ) => {
227+ if ( ! combinedController . signal . aborted ) {
228+ combinedController . abort ( ) ;
229+ }
230+ } ;
231+ options . signal . addEventListener ( 'abort' , abortHandler , { once : true } ) ;
232+ controller . signal . addEventListener ( 'abort' , abortHandler , { once : true } ) ;
170233 signal = combinedController . signal ;
171234 }
172235
@@ -213,11 +276,15 @@ export const supabase = createClient(supabaseUrl, supabasePublishableKey, {
213276
214277 // Immediately check for 429 rate limit on token refresh BEFORE returning
215278 if ( response . status === 429 && isTokenRefresh ) {
279+ // Increment consecutive rate limits and apply exponential backoff
280+ consecutiveRateLimits ++ ;
281+ rateLimitBackoff = Math . min ( 300000 , 30000 * Math . pow ( 2 , consecutiveRateLimits - 1 ) ) ; // Max 5 minutes
282+
216283 // Set the flag IMMEDIATELY
217284 ( window as any ) . __supabaseRateLimited = true ;
218- // Set rate limit for 30 seconds
219- rateLimitedUntil = Date . now ( ) + 30000 ;
220- console . error ( ' 🔐 Token refresh rate limited! Backing off for 30 seconds' ) ;
285+ // Set rate limit with exponential backoff
286+ rateLimitedUntil = Date . now ( ) + rateLimitBackoff ;
287+ console . error ( ` 🔐 Token refresh rate limited! Backing off for ${ rateLimitBackoff / 1000 } seconds (attempt ${ consecutiveRateLimits } )` ) ;
221288
222289 // Set a global flag so components know we're rate limited
223290 ( window as any ) . __supabaseRateLimited = true ;
@@ -226,6 +293,7 @@ export const supabase = createClient(supabaseUrl, supabasePublishableKey, {
226293 setTimeout ( async ( ) => {
227294 rateLimitedUntil = 0 ;
228295 ( window as any ) . __supabaseRateLimited = false ;
296+ consecutiveRateLimits = Math . max ( 0 , consecutiveRateLimits - 1 ) ; // Gradually reduce backoff
229297 console . log ( '🔐 Rate limit cleared, token refresh can resume' ) ;
230298
231299 // Try to restore the session if auth state was lost
@@ -300,6 +368,15 @@ export const supabase = createClient(supabaseUrl, supabasePublishableKey, {
300368 }
301369 }
302370
371+ // Reset rate limit counter on successful token refresh
372+ if ( response . ok && isTokenRefresh ) {
373+ consecutiveRateLimits = 0 ;
374+ rateLimitBackoff = 30000 ; // Reset to initial backoff
375+ // Clear the cache so next request gets fresh token
376+ cachedSessionToken = null ;
377+ cachedSessionExpiry = 0 ;
378+ }
379+
303380 return response ;
304381 } catch ( error ) {
305382 clearTimeout ( timeoutId ) ;
@@ -457,6 +534,37 @@ export const supabaseFunctions = {
457534 }
458535} ;
459536
537+ // Helper function to manually recover session after rate limit
538+ export const recoverSession = async ( ) => {
539+ try {
540+ // Check if we're still rate limited
541+ if ( rateLimitedUntil > Date . now ( ) ) {
542+ console . log ( '🔐 Still rate limited, waiting...' ) ;
543+ return false ;
544+ }
545+
546+ // Clear rate limit flag
547+ rateLimitedUntil = 0 ;
548+ delete ( window as any ) . __supabaseRateLimited ;
549+
550+ // Try to refresh the session
551+ const { data, error } = await supabase . auth . refreshSession ( ) ;
552+
553+ if ( ! error && data . session ) {
554+ console . log ( '🔐 Session recovered successfully' ) ;
555+ consecutiveRateLimits = 0 ; // Reset counter on success
556+ rateLimitBackoff = 30000 ; // Reset backoff
557+ return true ;
558+ } else {
559+ console . error ( '🔐 Failed to recover session:' , error ) ;
560+ return false ;
561+ }
562+ } catch ( error ) {
563+ console . error ( '🔐 Error recovering session:' , error ) ;
564+ return false ;
565+ }
566+ } ;
567+
460568// Helper functions for common operations
461569export const supabaseHelpers = {
462570 // Get or create API settings for a user (with actual API keys for settings page)
0 commit comments