@@ -163,6 +163,11 @@ export interface IAuthClient {
163163 * Cleanup the auth client if you no longer need it.
164164 */
165165 destroy ( ) : void
166+
167+ /**
168+ * Returns the auth options with all default values filled in.
169+ */
170+ getAuthOptions ( ) : IResolvedAuthOptions
166171}
167172
168173export interface IAuthOptions {
@@ -195,12 +200,20 @@ export interface IAuthOptions {
195200 /**
196201 * If true, disables the token refresh on initial page load.
197202 * Can help reduce duplicate token refresh requests.
198- *
203+ *
199204 * Default false
200205 */
201206 skipInitialFetch ?: boolean
202207}
203208
209+ export interface IResolvedAuthOptions {
210+ authUrl : string
211+ enableBackgroundTokenRefresh : boolean
212+ minSecondsBeforeRefresh : number
213+ disableRefreshOnFocus : boolean
214+ skipInitialFetch : boolean
215+ }
216+
204217interface AccessTokenActiveOrgMap {
205218 [ orgId : string ] : {
206219 accessToken : string
@@ -218,6 +231,8 @@ interface ClientState {
218231 refreshInterval : number | null
219232 lastRefresh : number | null
220233 accessTokenActiveOrgMap : AccessTokenActiveOrgMap
234+ pendingAuthRequest : Promise < AuthenticationInfo | null > | null
235+ pendingOrgAccessTokenRequests : Map < string , Promise < AccessTokenForActiveOrg > >
221236 readonly authUrl : string
222237}
223238
@@ -253,6 +268,8 @@ export function createClient(authOptions: IAuthOptions): IAuthClient {
253268 refreshInterval : null ,
254269 lastRefresh : null ,
255270 accessTokenActiveOrgMap : { } ,
271+ pendingAuthRequest : null ,
272+ pendingOrgAccessTokenRequests : new Map ( ) ,
256273 }
257274
258275 // Helper functions
@@ -331,23 +348,35 @@ export function createClient(authOptions: IAuthOptions): IAuthClient {
331348 }
332349
333350 async function forceRefreshToken ( returnCached : boolean ) : Promise < AuthenticationInfo | null > {
334- try {
335- // Happy case, we fetch auth info and save it
336- const authenticationInfo = await runWithRetriesOnAnyError ( ( ) =>
337- fetchAuthenticationInfo ( clientState . authUrl )
338- )
339- setAuthenticationInfoAndUpdateDownstream ( authenticationInfo )
340- return authenticationInfo
341- } catch ( e ) {
342- // If there was an error, we sometimes still want to return the value we have cached
343- // (e.g. if we were prefetching), so in those cases we swallow the exception
344- if ( returnCached ) {
345- return clientState . authenticationInfo
346- } else {
347- setAuthenticationInfoAndUpdateDownstream ( null )
348- throw e
349- }
351+ // If there's already an in-flight request, return it to avoid duplicate fetches
352+ if ( clientState . pendingAuthRequest ) {
353+ return clientState . pendingAuthRequest
350354 }
355+
356+ const request = ( async ( ) => {
357+ try {
358+ // Happy case, we fetch auth info and save it
359+ const authenticationInfo = await runWithRetriesOnAnyError ( ( ) =>
360+ fetchAuthenticationInfo ( clientState . authUrl )
361+ )
362+ setAuthenticationInfoAndUpdateDownstream ( authenticationInfo )
363+ return authenticationInfo
364+ } catch ( e ) {
365+ // If there was an error, we sometimes still want to return the value we have cached
366+ // (e.g. if we were prefetching), so in those cases we swallow the exception
367+ if ( returnCached ) {
368+ return clientState . authenticationInfo
369+ } else {
370+ setAuthenticationInfoAndUpdateDownstream ( null )
371+ throw e
372+ }
373+ } finally {
374+ clientState . pendingAuthRequest = null
375+ }
376+ } ) ( )
377+
378+ clientState . pendingAuthRequest = request
379+ return request
351380 }
352381
353382 const getSignupPageUrl = ( options ?: RedirectToSignupOptions ) => {
@@ -529,33 +558,47 @@ export function createClient(authOptions: IAuthOptions): IAuthClient {
529558 }
530559 }
531560 }
532- // Fetch the access token for the org ID and update.
533- try {
534- const authenticationInfo = await runWithRetriesOnAnyError ( ( ) =>
535- fetchAuthenticationInfo ( clientState . authUrl , orgId )
536- )
537- if ( ! authenticationInfo ) {
538- // Only null if 401 unauthorized.
561+
562+ // Check for in-flight request for this org to avoid duplicate fetches
563+ const pendingRequest = clientState . pendingOrgAccessTokenRequests . get ( orgId )
564+ if ( pendingRequest ) {
565+ return pendingRequest
566+ }
567+
568+ // Create new request and store it
569+ const request = ( async ( ) : Promise < AccessTokenForActiveOrg > => {
570+ try {
571+ const authenticationInfo = await runWithRetriesOnAnyError ( ( ) =>
572+ fetchAuthenticationInfo ( clientState . authUrl , orgId )
573+ )
574+ if ( ! authenticationInfo ) {
575+ // Only null if 401 unauthorized.
576+ return {
577+ error : "user_not_in_org" ,
578+ accessToken : null as never ,
579+ }
580+ }
581+ const { accessToken } = authenticationInfo
582+ clientState . accessTokenActiveOrgMap [ orgId ] = {
583+ accessToken,
584+ fetchedAt : currentTimeSecs ,
585+ }
586+ return {
587+ accessToken,
588+ error : undefined ,
589+ }
590+ } catch ( e ) {
539591 return {
540- error : "user_not_in_org " ,
592+ error : "unexpected_error " ,
541593 accessToken : null as never ,
542594 }
595+ } finally {
596+ clientState . pendingOrgAccessTokenRequests . delete ( orgId )
543597 }
544- const { accessToken } = authenticationInfo
545- clientState . accessTokenActiveOrgMap [ orgId ] = {
546- accessToken,
547- fetchedAt : currentTimeSecs ,
548- }
549- return {
550- accessToken,
551- error : undefined ,
552- }
553- } catch ( e ) {
554- return {
555- error : "unexpected_error" ,
556- accessToken : null as never ,
557- }
558- }
598+ } ) ( )
599+
600+ clientState . pendingOrgAccessTokenRequests . set ( orgId , request )
601+ return request
559602 } ,
560603
561604 getSignupPageUrl ( options ?: RedirectToSignupOptions ) : string {
@@ -626,6 +669,16 @@ export function createClient(authOptions: IAuthOptions): IAuthClient {
626669 clearInterval ( clientState . refreshInterval )
627670 }
628671 } ,
672+
673+ getAuthOptions ( ) : IResolvedAuthOptions {
674+ return {
675+ authUrl : clientState . authUrl ,
676+ enableBackgroundTokenRefresh : authOptions . enableBackgroundTokenRefresh ! ,
677+ minSecondsBeforeRefresh : minSecondsBeforeRefresh ,
678+ disableRefreshOnFocus : authOptions . disableRefreshOnFocus ?? false ,
679+ skipInitialFetch : authOptions . skipInitialFetch ?? false ,
680+ }
681+ } ,
629682 }
630683
631684 const onStorageChange = async function ( ) {
0 commit comments