Skip to content

Commit e5f213f

Browse files
authored
feat(clerk-js): Send touch intent with session updates (core-2 backport) (#8135)
1 parent c9cb1d8 commit e5f213f

6 files changed

Lines changed: 126 additions & 20 deletions

File tree

.changeset/warm-touch-intent.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@clerk/clerk-js': patch
3+
'@clerk/shared': patch
4+
---
5+
6+
Add optional `intent` parameter to `session.touch()` to indicate why the touch was triggered (focus, session switch, or org switch). This enables the backend to skip expensive client piggybacking for focus-only touches.

packages/clerk-js/src/core/__tests__/clerk.test.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ describe('Clerk singleton', () => {
204204
const sut = new Clerk(productionPublishableKey);
205205
await sut.load();
206206
await sut.setActive({ session: mockSession as any as ActiveSessionResource });
207-
expect(mockSession.touch).toHaveBeenCalled();
207+
expect(mockSession.touch).toHaveBeenCalledWith({ intent: 'select_session' });
208208
});
209209

210210
describe('with `touchSession` set to false', () => {
@@ -215,7 +215,7 @@ describe('Clerk singleton', () => {
215215
const sut = new Clerk(productionPublishableKey);
216216
await sut.load({ touchSession: false });
217217
await sut.setActive({ session: mockSession as any as ActiveSessionResource });
218-
expect(mockSession.touch).toHaveBeenCalled();
218+
expect(mockSession.touch).toHaveBeenCalledWith({ intent: 'select_session' });
219219
});
220220
});
221221

@@ -230,7 +230,7 @@ describe('Clerk singleton', () => {
230230
const sut = new Clerk(productionPublishableKey);
231231
await sut.load();
232232
await sut.setActive({ session: mockSession as any as ActiveSessionResource });
233-
expect(mockSession.touch).toHaveBeenCalled();
233+
expect(mockSession.touch).toHaveBeenCalledWith({ intent: 'select_session' });
234234
});
235235

236236
it('sets __session and __client_uat cookie before calling __unstable__onBeforeSetActive', async () => {
@@ -252,7 +252,7 @@ describe('Clerk singleton', () => {
252252
mockClientFetch.mockReturnValue(Promise.resolve({ signedInSessions: [mockSession] }));
253253

254254
(window as any).__unstable__onAfterSetActive = () => {
255-
expect(mockSession.touch).toHaveBeenCalled();
255+
expect(mockSession.touch).toHaveBeenCalledWith({ intent: 'select_session' });
256256
expect(beforeEmitMock).toHaveBeenCalled();
257257
};
258258

@@ -299,7 +299,7 @@ describe('Clerk singleton', () => {
299299

300300
await waitFor(() => {
301301
expect(executionOrder).toEqual(['session.touch', 'set cookie', 'before emit']);
302-
expect(mockSession2.touch).toHaveBeenCalled();
302+
expect(mockSession2.touch).toHaveBeenCalledWith({ intent: 'select_session' });
303303
expect(mockSession2.getToken).toHaveBeenCalled();
304304
expect(beforeEmitMock).toHaveBeenCalledWith(mockSession2);
305305
expect(sut.session).toMatchObject(mockSession2);
@@ -332,7 +332,7 @@ describe('Clerk singleton', () => {
332332

333333
await waitFor(() => {
334334
expect(executionOrder).toEqual(['session.touch', 'set cookie', 'before emit']);
335-
expect(mockSession.touch).toHaveBeenCalled();
335+
expect(mockSession.touch).toHaveBeenCalledWith({ intent: 'select_org' });
336336
expect(mockSession.getToken).toHaveBeenCalled();
337337
expect((mockSession as any as ActiveSessionResource)?.lastActiveOrganizationId).toEqual('org_id');
338338
expect(beforeEmitMock).toHaveBeenCalledWith(mockSession);
@@ -371,7 +371,7 @@ describe('Clerk singleton', () => {
371371
await sut.setActive({ organization: 'some-org-slug' });
372372

373373
await waitFor(() => {
374-
expect(mockSession2.touch).toHaveBeenCalled();
374+
expect(mockSession2.touch).toHaveBeenCalledWith({ intent: 'select_org' });
375375
expect(mockSession2.getToken).toHaveBeenCalled();
376376
expect((mockSession2 as any as ActiveSessionResource)?.lastActiveOrganizationId).toEqual('org_id');
377377
expect(sut.session).toMatchObject(mockSession2);
@@ -454,7 +454,7 @@ describe('Clerk singleton', () => {
454454
const sut = new Clerk(productionPublishableKey);
455455
await sut.load();
456456
await sut.setActive({ session: mockSession as any as PendingSessionResource, navigate });
457-
expect(mockSession.touch).toHaveBeenCalled();
457+
expect(mockSession.touch).toHaveBeenCalledWith({ intent: 'select_session' });
458458
expect(navigate).toHaveBeenCalled();
459459
});
460460

@@ -479,7 +479,7 @@ describe('Clerk singleton', () => {
479479
await sut.setActive({ organization: { id: 'org_id' } as Organization, beforeEmit: beforeEmitMock });
480480

481481
expect(executionOrder).toEqual(['session.touch', 'before emit']);
482-
expect(mockSession.touch).toHaveBeenCalled();
482+
expect(mockSession.touch).toHaveBeenCalledWith({ intent: 'select_org' });
483483
expect((mockSession as any as ActiveSessionResource)?.lastActiveOrganizationId).toEqual('org_id');
484484
expect(mockSession.getToken).toHaveBeenCalled();
485485
expect(beforeEmitMock).toHaveBeenCalledWith(mockSession);
@@ -534,7 +534,7 @@ describe('Clerk singleton', () => {
534534
const sut = new Clerk(productionPublishableKey);
535535
await sut.load();
536536
await sut.setActive({ session: mockSession as any as PendingSessionResource });
537-
expect(mockSession.touch).toHaveBeenCalled();
537+
expect(mockSession.touch).toHaveBeenCalledWith({ intent: 'select_session' });
538538
});
539539

540540
it('does not call __unstable__onBeforeSetActive before session.touch', async () => {
@@ -575,7 +575,7 @@ describe('Clerk singleton', () => {
575575
},
576576
});
577577
await sut.setActive({ session: mockSession as any as PendingSessionResource });
578-
expect(mockSession.touch).toHaveBeenCalled();
578+
expect(mockSession.touch).toHaveBeenCalledWith({ intent: 'select_session' });
579579
expect(sut.navigate).toHaveBeenCalledWith('/choose-organization');
580580
});
581581

@@ -587,7 +587,7 @@ describe('Clerk singleton', () => {
587587
const sut = new Clerk(productionPublishableKey);
588588
await sut.load();
589589
await sut.setActive({ session: mockSession as any as PendingSessionResource, navigate });
590-
expect(mockSession.touch).toHaveBeenCalled();
590+
expect(mockSession.touch).toHaveBeenCalledWith({ intent: 'select_session' });
591591
expect(navigate).toHaveBeenCalled();
592592
});
593593
});
@@ -660,7 +660,7 @@ describe('Clerk singleton', () => {
660660
await sut.setActive({ organization: 'some-org-slug' });
661661

662662
await waitFor(() => {
663-
expect(mockSessionWithOrganization.touch).toHaveBeenCalled();
663+
expect(mockSessionWithOrganization.touch).toHaveBeenCalledWith({ intent: 'select_org' });
664664
expect(mockSessionWithOrganization.getToken).toHaveBeenCalled();
665665
expect((mockSessionWithOrganization as any as ActiveSessionResource)?.lastActiveOrganizationId).toEqual(
666666
'org_id',

packages/clerk-js/src/core/clerk.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ import type {
7373
Resources,
7474
SDKMetadata,
7575
SessionResource,
76+
SessionTouchParams,
7677
SetActiveParams,
7778
SignedInSessionResource,
7879
SignInProps,
@@ -1555,11 +1556,13 @@ export class Clerk implements ClerkInterface {
15551556
await onBeforeSetActive(newSession === null ? 'sign-out' : undefined);
15561557
}
15571558

1559+
const touchIntent: SessionTouchParams['intent'] = shouldSwitchOrganization ? 'select_org' : 'select_session';
1560+
15581561
//1. setLastActiveSession to passed user session (add a param).
15591562
// Note that this will also update the session's active organization
15601563
// id.
15611564
if (inActiveBrowserTab() || !this.#options.standardBrowser) {
1562-
await this.#touchCurrentSession(newSession);
1565+
await this.#touchCurrentSession(newSession, touchIntent);
15631566
// reload session from updated client
15641567
newSession = this.#getSessionFromClient(newSession?.id);
15651568
}
@@ -3016,7 +3019,7 @@ export class Clerk implements ClerkInterface {
30163019
this.#touchThrottledUntil = Date.now() + 5_000;
30173020

30183021
if (this.#options.touchSession) {
3019-
void this.#touchCurrentSession(this.session);
3022+
void this.#touchCurrentSession(this.session, 'focus');
30203023
}
30213024
});
30223025

@@ -3047,12 +3050,15 @@ export class Clerk implements ClerkInterface {
30473050
};
30483051

30493052
// TODO: Be more conservative about touches. Throttle, don't touch when only one user, etc
3050-
#touchCurrentSession = async (session?: SignedInSessionResource | null): Promise<void> => {
3053+
#touchCurrentSession = async (
3054+
session?: SignedInSessionResource | null,
3055+
intent: SessionTouchParams['intent'] = 'focus',
3056+
): Promise<void> => {
30513057
if (!session) {
30523058
return Promise.resolve();
30533059
}
30543060

3055-
await session.touch().catch(e => {
3061+
await session.touch({ intent }).catch(e => {
30563062
if (is4xxError(e)) {
30573063
void this.handleUnauthenticated();
30583064
}

packages/clerk-js/src/core/resources/Session.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type {
1515
SessionResource,
1616
SessionStatus,
1717
SessionTask,
18+
SessionTouchParams,
1819
SessionVerificationJSON,
1920
SessionVerificationResource,
2021
SessionVerifyAttemptFirstFactorParams,
@@ -86,10 +87,10 @@ export class Session extends BaseResource implements SessionResource {
8687
});
8788
};
8889

89-
touch = (): Promise<SessionResource> => {
90+
touch = ({ intent }: SessionTouchParams = {}): Promise<SessionResource> => {
9091
return this._basePost({
9192
action: 'touch',
92-
body: { active_organization_id: this.lastActiveOrganizationId },
93+
body: { active_organization_id: this.lastActiveOrganizationId, intent },
9394
}).then(res => {
9495
// touch() will potentially change the session state, and so we need to ensure we emit the updated token that comes back in the response. This avoids potential issues where the session cookie is out of sync with the current session state.
9596
if (res.lastActiveToken) {

packages/clerk-js/src/core/resources/__tests__/Session.test.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,93 @@ describe('Session', () => {
467467
token: session.lastActiveToken,
468468
});
469469
});
470+
471+
it('passes touch intent in the request body', async () => {
472+
const sessionData = {
473+
status: 'active',
474+
id: 'session_1',
475+
object: 'session',
476+
user: createUser({}),
477+
last_active_organization_id: 'org_123',
478+
actor: null,
479+
created_at: new Date().getTime(),
480+
updated_at: new Date().getTime(),
481+
} as SessionJSON;
482+
const session = new Session(sessionData);
483+
484+
const requestSpy = BaseResource.clerk.getFapiClient().request as Mock;
485+
requestSpy.mockResolvedValue({
486+
payload: session,
487+
});
488+
489+
await session.touch({ intent: 'focus' });
490+
491+
expect(requestSpy).toHaveBeenCalledWith(
492+
expect.objectContaining({
493+
body: { active_organization_id: 'org_123', intent: 'focus' },
494+
method: 'POST',
495+
}),
496+
expect.anything(),
497+
);
498+
});
499+
500+
it('passes select_session intent in the request body', async () => {
501+
const sessionData = {
502+
status: 'active',
503+
id: 'session_1',
504+
object: 'session',
505+
user: createUser({}),
506+
last_active_organization_id: 'org_123',
507+
actor: null,
508+
created_at: new Date().getTime(),
509+
updated_at: new Date().getTime(),
510+
} as SessionJSON;
511+
const session = new Session(sessionData);
512+
513+
const requestSpy = BaseResource.clerk.getFapiClient().request as Mock;
514+
requestSpy.mockResolvedValue({
515+
payload: session,
516+
});
517+
518+
await session.touch({ intent: 'select_session' });
519+
520+
expect(requestSpy).toHaveBeenCalledWith(
521+
expect.objectContaining({
522+
body: { active_organization_id: 'org_123', intent: 'select_session' },
523+
method: 'POST',
524+
}),
525+
expect.anything(),
526+
);
527+
});
528+
529+
it('passes select_org intent in the request body', async () => {
530+
const sessionData = {
531+
status: 'active',
532+
id: 'session_1',
533+
object: 'session',
534+
user: createUser({}),
535+
last_active_organization_id: 'org_123',
536+
actor: null,
537+
created_at: new Date().getTime(),
538+
updated_at: new Date().getTime(),
539+
} as SessionJSON;
540+
const session = new Session(sessionData);
541+
542+
const requestSpy = BaseResource.clerk.getFapiClient().request as Mock;
543+
requestSpy.mockResolvedValue({
544+
payload: session,
545+
});
546+
547+
await session.touch({ intent: 'select_org' });
548+
549+
expect(requestSpy).toHaveBeenCalledWith(
550+
expect.objectContaining({
551+
body: { active_organization_id: 'org_123', intent: 'select_org' },
552+
method: 'POST',
553+
}),
554+
expect.anything(),
555+
);
556+
});
470557
});
471558

472559
describe('isAuthorized()', () => {

packages/shared/src/types/session.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ export interface SessionResource extends ClerkResource {
239239
*/
240240
end: () => Promise<SessionResource>;
241241
remove: () => Promise<SessionResource>;
242-
touch: () => Promise<SessionResource>;
242+
touch: (params?: SessionTouchParams) => Promise<SessionResource>;
243243
getToken: GetToken;
244244
checkAuthorization: CheckAuthorization;
245245
clearCache: () => void;
@@ -320,6 +320,12 @@ export type SessionStatus =
320320
| 'revoked'
321321
| 'pending';
322322

323+
export type SessionTouchIntent = 'focus' | 'select_session' | 'select_org';
324+
325+
export type SessionTouchParams = {
326+
intent?: SessionTouchIntent;
327+
};
328+
323329
export interface PublicUserData {
324330
firstName: string | null;
325331
lastName: string | null;

0 commit comments

Comments
 (0)