Skip to content
17 changes: 15 additions & 2 deletions packages/clerk-js/src/core/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1412,7 +1412,7 @@ export class Clerk implements ClerkInterface {
return;
}

if (newSession?.status !== 'pending') {
if (newSession?.status !== 'pending' && this.session?.id !== newSession?.id) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we want the transitive state to work when switching between orgs, doesn't this affect that since sessionId will remain the same in that scenario

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I believe that is correct. This logic does not account for switching orgs. 🤔

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we know how the transitive state cause a null ? Because it usually means undefined is emitted

this.#setTransitiveState();
}

Expand Down Expand Up @@ -2351,7 +2351,9 @@ export class Clerk implements ClerkInterface {
}

updateClient = (newClient: ClientResource): void => {
if (!this.client) {
const isFirstClientSet = !this.client;

if (isFirstClientSet) {
// This is the first time client is being
// set, so we also need to set session
const session = this.#options.selectInitialSession
Expand Down Expand Up @@ -2381,6 +2383,11 @@ export class Clerk implements ClerkInterface {

// A client response contains its associated sessions, along with a fresh token, so we dispatch a token update event.
eventBus.emit(events.TokenUpdate, { token: this.session?.lastActiveToken });
} else if (!isFirstClientSet && newClient.sessions?.length > 0) {
const session = this.#options.selectInitialSession
? this.#options.selectInitialSession(newClient)
: this.#defaultSession(newClient);
this.#setAccessors(session);
Comment on lines 2424 to 2431
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we know why this is new case is necessary? The client is being updated, which has a new session to be set, but the session accessor wasn't yet set elsewhere?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it is. This handles the case where updateClient() is executed during touch(). In that call we get the piggybacked client data. The key here is that this is an else branch of if (this.session) { which we do not have at this point.

So without this else, we do nothing here and just this.#emit(); an empty session.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh! Had to dig through the code and think for a bit to get it, but:

  • First updateClient is guaranteed to set a session
  • But if session is later removed, for example because of transitive state..
  • ..and updateClient is called during that time (touch), we currently don't set the new session?

Very nice find. Adding a comment might be helpful here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be covered by updateClient ?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's what this PR adds. It might be easier to grok looking at it like this:

  updateClient = (newClient: ClientResource): void => {
    const isFirstClientSet = !this.client;

    if (isFirstClientSet) {
      // If first time setting client, also set session
      // Note: This does _not_ trigger during transitive state as we still have a client
    }
    this.client = newClient;

    if (this.session) {
      // Previously we only set a new session if we already had an active session,
      // which we don't during transitive state
    } else if (!isFirstClientSet && newClient.sessions?.length > 0) {
      // Now, if client is set, but not session, and there is a new session to set, set it
    }

    this.#emit();
  };

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Ephem that's a pretty good summary. This is my mental model of this mechanism:

updateClient = (newClient: ClientResource): void => {
  const isFirstClientSet = !this.client;

  if (isFirstClientSet) {
    // First time setting client (during initialization)
    // Also set the initial session from the new client
  }

  this.client = newClient;

  if (this.session) {
    // We have an active session - refresh it with updated data from newClient
    // This is the normal path during regular operation
  } else if (!isFirstClientSet && newClient.sessions?.length > 0) {
    // Race condition recovery path:
    // - Client was already set (so NOT first initialization)
    // - Session is currently falsy (e.g., cleared by setTransitiveState during setActive)
    // - BUT newClient has session data (e.g., from touch() response that piggybacked)
    // → Restore the session immediately to prevent null flash
  }

  this.#emit();
};

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking about this some more, I realize I still don't understand how the this.session goes from undefined to null because of this? Shouldn't the this.#emit be emitting the existing session, which is undefined? 🤔

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I found the reason here: #7157 (comment)

(Just crossposting here to keep a good history if someone stumbles across this in the future)

}

this.#emit();
Expand Down Expand Up @@ -2591,6 +2598,12 @@ export class Clerk implements ClerkInterface {
});

const initClient = async () => {
const jwtInCookie = this.#authService?.getSessionCookie();
if (jwtInCookie) {
const preliminaryClient = createClientFromJwt(jwtInCookie);
this.updateClient(preliminaryClient);
}

return Client.getOrCreateInstance()
.fetch()
.then(res => this.updateClient(res))
Expand Down
Loading