Skip to content

Commit 4ae8530

Browse files
authored
feat(passport): ID-3764 Debounce login (#2651)
1 parent 00e74ed commit 4ae8530

File tree

2 files changed

+105
-1
lines changed

2 files changed

+105
-1
lines changed

packages/passport/sdk/src/Passport.test.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,94 @@ describe('Passport', () => {
583583
expect(getUserMock).toBeCalledTimes(1);
584584
expect(authLoginMock).toBeCalledTimes(1);
585585
});
586+
587+
it('should debounce concurrent login calls and return the same result', async () => {
588+
getUserMock.mockReturnValue(null);
589+
authLoginMock.mockReturnValue(mockUserImx);
590+
591+
// Make multiple concurrent login calls
592+
const loginPromise1 = passport.login();
593+
const loginPromise2 = passport.login();
594+
const loginPromise3 = passport.login();
595+
596+
// All promises should be the same reference
597+
expect(loginPromise1).toEqual(loginPromise2);
598+
expect(loginPromise2).toEqual(loginPromise3);
599+
600+
// Wait for all to complete
601+
const [result1, result2, result3] = await Promise.all([
602+
loginPromise1,
603+
loginPromise2,
604+
loginPromise3,
605+
]);
606+
607+
// All results should be the same
608+
expect(result1).toEqual(mockUserImx.profile);
609+
expect(result2).toEqual(mockUserImx.profile);
610+
expect(result3).toEqual(mockUserImx.profile);
611+
612+
// AuthManager.login should only be called once despite multiple login calls
613+
expect(authLoginMock).toBeCalledTimes(1);
614+
});
615+
616+
it('should reset login promise after successful completion and allow new login calls', async () => {
617+
getUserMock.mockReturnValue(null);
618+
authLoginMock.mockReturnValue(mockUserImx);
619+
620+
// First login call
621+
const firstLoginResult = await passport.login();
622+
expect(firstLoginResult).toEqual(mockUserImx.profile);
623+
624+
// Second login call after first completes should create a new login process
625+
const secondLoginResult = await passport.login();
626+
expect(secondLoginResult).toEqual(mockUserImx.profile);
627+
628+
// AuthManager.login should be called twice (once for each login)
629+
expect(authLoginMock).toBeCalledTimes(2);
630+
});
631+
632+
it('should reset login promise after failed completion and allow new login calls', async () => {
633+
const error = new Error('Login failed');
634+
getUserMock.mockReturnValue(null);
635+
authLoginMock.mockRejectedValue(error);
636+
637+
// First login call should fail
638+
await expect(passport.login()).rejects.toThrow(error);
639+
640+
// Second login call after first fails should create a new login process
641+
authLoginMock.mockReturnValue(mockUserImx);
642+
const secondLoginResult = await passport.login();
643+
expect(secondLoginResult).toEqual(mockUserImx.profile);
644+
645+
// AuthManager.login should be called twice (once for failed, once for successful)
646+
expect(authLoginMock).toBeCalledTimes(2);
647+
});
648+
649+
it('should debounce concurrent login calls even when they fail', async () => {
650+
const error = new Error('Login failed');
651+
getUserMock.mockReturnValue(null);
652+
authLoginMock.mockRejectedValue(error);
653+
654+
// Make multiple concurrent login calls that will fail
655+
const [loginPromise1, loginPromise2, loginPromise3] = [
656+
passport.login(),
657+
passport.login(),
658+
passport.login(),
659+
];
660+
661+
// All promises should be the same reference
662+
expect(loginPromise1).toEqual(loginPromise2);
663+
expect(loginPromise2).toEqual(loginPromise3);
664+
665+
// All should reject with the same error
666+
try {
667+
await Promise.all([loginPromise1, loginPromise2, loginPromise3]);
668+
} catch (e: unknown) {
669+
expect(e).toEqual(error);
670+
// AuthManager.login should only be called once despite multiple login calls
671+
expect(authLoginMock).toBeCalledTimes(1);
672+
}
673+
});
586674
});
587675

588676
describe('linkExternalWallet', () => {

packages/passport/sdk/src/Passport.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,8 @@ export class Passport {
183183
}, 'connectEvm', false);
184184
}
185185

186+
#loginPromise: Promise<UserProfile | null> | null = null;
187+
186188
/**
187189
* Initiates the login process.
188190
* @param {Object} options - Login options
@@ -200,7 +202,13 @@ export class Passport {
200202
useSilentLogin?: boolean;
201203
useRedirectFlow?: boolean;
202204
}): Promise<UserProfile | null> {
203-
return withMetricsAsync(async () => {
205+
// If there's already a login in progress, return that promise
206+
if (this.#loginPromise) {
207+
return this.#loginPromise;
208+
}
209+
210+
// Create and store the login promise
211+
this.#loginPromise = withMetricsAsync(async () => {
204212
const { useCachedSession = false, useSilentLogin } = options || {};
205213
let user: User | null = null;
206214

@@ -235,6 +243,14 @@ export class Passport {
235243

236244
return user ? user.profile : null;
237245
}, 'login');
246+
247+
try {
248+
const result = await this.#loginPromise;
249+
return result;
250+
} finally {
251+
// Reset the login promise when the login process completes
252+
this.#loginPromise = null;
253+
}
238254
}
239255

240256
/**

0 commit comments

Comments
 (0)