Skip to content

Commit 3b9b877

Browse files
Add getAuthOptions to expose resolve options for client (#42)
* Add getAuthOptions to expose resolve options for client * Deduplicate in-flight requests for refreshes and org-scoped refreshes
1 parent b3355c1 commit 3b9b877

File tree

4 files changed

+241
-47
lines changed

4 files changed

+241
-47
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"type": "git",
66
"url": "https://github.com/PropelAuth/javascript"
77
},
8-
"version": "2.0.23",
8+
"version": "2.0.24",
99
"keywords": [
1010
"auth",
1111
"user",

src/client.ts

Lines changed: 93 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -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

168173
export 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+
204217
interface 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 () {

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export type {
55
AccessTokenForActiveOrg,
66
IAuthClient,
77
IAuthOptions,
8+
IResolvedAuthOptions,
89
RedirectToAccountOptions,
910
RedirectToCreateOrgOptions,
1011
RedirectToLoginOptions,

0 commit comments

Comments
 (0)