Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions packages/passport/sdk/src/Passport.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,94 @@ describe('Passport', () => {
expect(getUserMock).toBeCalledTimes(1);
expect(authLoginMock).toBeCalledTimes(1);
});

it('should debounce concurrent login calls and return the same result', async () => {
getUserMock.mockReturnValue(null);
authLoginMock.mockReturnValue(mockUserImx);

// Make multiple concurrent login calls
const loginPromise1 = passport.login();
const loginPromise2 = passport.login();
const loginPromise3 = passport.login();

// All promises should be the same reference
expect(loginPromise1).toEqual(loginPromise2);
expect(loginPromise2).toEqual(loginPromise3);

// Wait for all to complete
const [result1, result2, result3] = await Promise.all([
loginPromise1,
loginPromise2,
loginPromise3,
]);

// All results should be the same
expect(result1).toEqual(mockUserImx.profile);
expect(result2).toEqual(mockUserImx.profile);
expect(result3).toEqual(mockUserImx.profile);

// AuthManager.login should only be called once despite multiple login calls
expect(authLoginMock).toBeCalledTimes(1);
});

it('should reset login promise after successful completion and allow new login calls', async () => {
getUserMock.mockReturnValue(null);
authLoginMock.mockReturnValue(mockUserImx);

// First login call
const firstLoginResult = await passport.login();
expect(firstLoginResult).toEqual(mockUserImx.profile);

// Second login call after first completes should create a new login process
const secondLoginResult = await passport.login();
expect(secondLoginResult).toEqual(mockUserImx.profile);

// AuthManager.login should be called twice (once for each login)
expect(authLoginMock).toBeCalledTimes(2);
});

it('should reset login promise after failed completion and allow new login calls', async () => {
const error = new Error('Login failed');
getUserMock.mockReturnValue(null);
authLoginMock.mockRejectedValue(error);

// First login call should fail
await expect(passport.login()).rejects.toThrow(error);

// Second login call after first fails should create a new login process
authLoginMock.mockReturnValue(mockUserImx);
const secondLoginResult = await passport.login();
expect(secondLoginResult).toEqual(mockUserImx.profile);

// AuthManager.login should be called twice (once for failed, once for successful)
expect(authLoginMock).toBeCalledTimes(2);
});

it('should debounce concurrent login calls even when they fail', async () => {
const error = new Error('Login failed');
getUserMock.mockReturnValue(null);
authLoginMock.mockRejectedValue(error);

// Make multiple concurrent login calls that will fail
const [loginPromise1, loginPromise2, loginPromise3] = [
passport.login(),
passport.login(),
passport.login(),
];

// All promises should be the same reference
expect(loginPromise1).toEqual(loginPromise2);
expect(loginPromise2).toEqual(loginPromise3);

// All should reject with the same error
try {
await Promise.all([loginPromise1, loginPromise2, loginPromise3]);
} catch (e: unknown) {
expect(e).toEqual(error);
// AuthManager.login should only be called once despite multiple login calls
expect(authLoginMock).toBeCalledTimes(1);
}
});
});

describe('linkExternalWallet', () => {
Expand Down
18 changes: 17 additions & 1 deletion packages/passport/sdk/src/Passport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,8 @@ export class Passport {
}, 'connectEvm', false);
}

#loginPromise: Promise<UserProfile | null> | null = null;

/**
* Initiates the login process.
* @param {Object} options - Login options
Expand All @@ -200,7 +202,13 @@ export class Passport {
useSilentLogin?: boolean;
useRedirectFlow?: boolean;
}): Promise<UserProfile | null> {
return withMetricsAsync(async () => {
// If there's already a login in progress, return that promise
if (this.#loginPromise) {
return this.#loginPromise;
}

// Create and store the login promise
this.#loginPromise = withMetricsAsync(async () => {
const { useCachedSession = false, useSilentLogin } = options || {};
let user: User | null = null;

Expand Down Expand Up @@ -235,6 +243,14 @@ export class Passport {

return user ? user.profile : null;
}, 'login');

try {
const result = await this.#loginPromise;
return result;
} finally {
// Reset the login promise when the login process completes
this.#loginPromise = null;
}
}

/**
Expand Down
Loading