Skip to content

Commit e00ec97

Browse files
committed
Reapply "feat(clerk-js): send touch intent with session updates (#8101)"
This reverts commit ce67184.
1 parent f0533a2 commit e00ec97

File tree

6 files changed

+90
-18
lines changed

6 files changed

+90
-18
lines changed

.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: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ describe('Clerk singleton', () => {
207207
const sut = new Clerk(productionPublishableKey);
208208
await sut.load();
209209
await sut.setActive({ session: mockSession as any as ActiveSessionResource });
210-
expect(mockSession.touch).toHaveBeenCalled();
210+
expect(mockSession.touch).toHaveBeenCalledWith({ intent: 'select_session' });
211211
});
212212

213213
describe('with `touchSession` set to false', () => {
@@ -218,7 +218,7 @@ describe('Clerk singleton', () => {
218218
const sut = new Clerk(productionPublishableKey);
219219
await sut.load({ touchSession: false });
220220
await sut.setActive({ session: mockSession as any as ActiveSessionResource });
221-
expect(mockSession.touch).toHaveBeenCalled();
221+
expect(mockSession.touch).toHaveBeenCalledWith({ intent: 'select_session' });
222222
});
223223
});
224224

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

239239
it('sets __session and __client_uat cookie before calling __internal_onBeforeSetActive', async () => {
@@ -280,7 +280,7 @@ describe('Clerk singleton', () => {
280280
await sut.setActive({ organization: 'some-org-slug' });
281281

282282
await waitFor(() => {
283-
expect(mockSession2.touch).toHaveBeenCalled();
283+
expect(mockSession2.touch).toHaveBeenCalledWith({ intent: 'select_org' });
284284
expect(mockSession2.getToken).toHaveBeenCalled();
285285
expect((mockSession2 as any as ActiveSessionResource)?.lastActiveOrganizationId).toEqual('org_id');
286286
expect(sut.session).toMatchObject(mockSession2);
@@ -363,7 +363,7 @@ describe('Clerk singleton', () => {
363363
const sut = new Clerk(productionPublishableKey);
364364
await sut.load();
365365
await sut.setActive({ session: mockSession as any as PendingSessionResource, navigate });
366-
expect(mockSession.__internal_touch).toHaveBeenCalled();
366+
expect(mockSession.__internal_touch).toHaveBeenCalledWith({ intent: 'select_session' });
367367
expect(navigate).toHaveBeenCalled();
368368
});
369369

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

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ import type {
101101
Resources,
102102
SDKMetadata,
103103
SessionResource,
104+
SessionTouchParams,
104105
SetActiveParams,
105106
SignedInSessionResource,
106107
SignInProps,
@@ -1579,6 +1580,7 @@ export class Clerk implements ClerkInterface {
15791580
newSession?.currentTask &&
15801581
this.#options.taskUrls?.[newSession?.currentTask.key];
15811582
const shouldNavigate = !!(redirectUrl || taskUrl || setActiveNavigate);
1583+
const touchIntent: SessionTouchParams['intent'] = shouldSwitchOrganization ? 'select_org' : 'select_session';
15821584

15831585
//1. setLastActiveSession to passed user session (add a param).
15841586
// Note that this will also update the session's active organization
@@ -1599,7 +1601,7 @@ export class Clerk implements ClerkInterface {
15991601
if (shouldNavigate && newSession) {
16001602
try {
16011603
// __internal_touch does not call updateClient automatically
1602-
updatedClient = await newSession.__internal_touch();
1604+
updatedClient = await newSession.__internal_touch({ intent: touchIntent });
16031605
if (updatedClient) {
16041606
// We call updateClient manually, but without letting it emit
16051607
// It's important that the setTransitiveState call happens somewhat
@@ -1615,7 +1617,7 @@ export class Clerk implements ClerkInterface {
16151617
}
16161618
}
16171619
} else {
1618-
await this.#touchCurrentSession(newSession);
1620+
await this.#touchCurrentSession(newSession, touchIntent);
16191621
}
16201622
// If we do have the updatedClient, read from that, otherwise getSessionFromClient
16211623
// will fallback to this.client. This makes no difference now, but will if we
@@ -3150,7 +3152,7 @@ export class Clerk implements ClerkInterface {
31503152
this.#touchThrottledUntil = Date.now() + 5_000;
31513153

31523154
if (this.#options.touchSession) {
3153-
void this.#touchCurrentSession(this.session);
3155+
void this.#touchCurrentSession(this.session, 'focus');
31543156
}
31553157
});
31563158

@@ -3181,12 +3183,15 @@ export class Clerk implements ClerkInterface {
31813183
};
31823184

31833185
// TODO: Be more conservative about touches. Throttle, don't touch when only one user, etc
3184-
#touchCurrentSession = async (session?: SignedInSessionResource | null): Promise<void> => {
3186+
#touchCurrentSession = async (
3187+
session?: SignedInSessionResource | null,
3188+
intent: SessionTouchParams['intent'] = 'focus',
3189+
): Promise<void> => {
31853190
if (!session) {
31863191
return Promise.resolve();
31873192
}
31883193

3189-
await session.touch().catch(e => {
3194+
await session.touch({ intent }).catch(e => {
31903195
if (isUnauthenticatedError(e)) {
31913196
void this.handleUnauthenticated();
31923197
} else {

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

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import type {
2929
SessionResource,
3030
SessionStatus,
3131
SessionTask,
32+
SessionTouchParams,
3233
SessionVerificationJSON,
3334
SessionVerificationResource,
3435
SessionVerifyAttemptFirstFactorParams,
@@ -103,14 +104,16 @@ export class Session extends BaseResource implements SessionResource {
103104
};
104105

105106
private _touchPost = async (
106-
{ skipUpdateClient }: { skipUpdateClient: boolean } = { skipUpdateClient: false },
107+
{ intent, skipUpdateClient }: { intent?: SessionTouchParams['intent']; skipUpdateClient: boolean } = {
108+
skipUpdateClient: false,
109+
},
107110
): Promise<FapiResponseJSON<SessionJSON> | null> => {
108111
const json = await BaseResource._fetch<SessionJSON>(
109112
{
110113
method: 'POST',
111114
path: this.path('touch'),
112115
// any is how we type the body in the BaseMutateParams as well
113-
body: { active_organization_id: this.lastActiveOrganizationId } as any,
116+
body: { active_organization_id: this.lastActiveOrganizationId, intent } as any,
114117
},
115118
{ skipUpdateClient },
116119
);
@@ -121,8 +124,8 @@ export class Session extends BaseResource implements SessionResource {
121124
return json;
122125
};
123126

124-
touch = async (): Promise<SessionResource> => {
125-
await this._touchPost();
127+
touch = async ({ intent }: SessionTouchParams = {}): Promise<SessionResource> => {
128+
await this._touchPost({ intent, skipUpdateClient: false });
126129

127130
// _touchPost() will have updated `this` in-place
128131
// The post has potentially changed 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.
@@ -143,8 +146,8 @@ export class Session extends BaseResource implements SessionResource {
143146
*
144147
* @internal
145148
*/
146-
__internal_touch = async (): Promise<ClientResource | undefined> => {
147-
const json = await this._touchPost({ skipUpdateClient: true });
149+
__internal_touch = async ({ intent }: SessionTouchParams = {}): Promise<ClientResource | undefined> => {
150+
const json = await this._touchPost({ intent, skipUpdateClient: true });
148151
return getClientResourceFromPayload(json);
149152
};
150153

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

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -798,6 +798,37 @@ describe('Session', () => {
798798
token: session.lastActiveToken,
799799
});
800800
});
801+
802+
it('passes touch intent in the request body', async () => {
803+
const sessionData = {
804+
status: 'active',
805+
id: 'session_1',
806+
object: 'session',
807+
user: createUser({}),
808+
last_active_organization_id: 'org_123',
809+
actor: null,
810+
created_at: new Date().getTime(),
811+
updated_at: new Date().getTime(),
812+
} as SessionJSON;
813+
const session = new Session(sessionData);
814+
815+
const requestSpy = BaseResource.clerk.getFapiClient().request as Mock;
816+
requestSpy.mockResolvedValue({
817+
payload: { response: sessionData },
818+
status: 200,
819+
});
820+
821+
await session.touch({ intent: 'focus' });
822+
823+
expect(requestSpy).toHaveBeenCalledWith(
824+
expect.objectContaining({
825+
body: { active_organization_id: 'org_123', intent: 'focus' },
826+
method: 'POST',
827+
path: '/client/sessions/session_1/touch',
828+
}),
829+
expect.anything(),
830+
);
831+
});
801832
});
802833

803834
describe('__internal_touch()', () => {
@@ -902,6 +933,27 @@ describe('Session', () => {
902933

903934
expect(session.lastActiveOrganizationId).toBe('org_456');
904935
});
936+
937+
it('passes touch intent in the request body', async () => {
938+
const session = new Session(mockSessionData);
939+
const requestSpy = BaseResource.clerk.getFapiClient().request as Mock;
940+
941+
requestSpy.mockResolvedValue({
942+
payload: { response: mockSessionData },
943+
status: 200,
944+
});
945+
946+
await session.__internal_touch({ intent: 'select_session' });
947+
948+
expect(requestSpy).toHaveBeenCalledWith(
949+
expect.objectContaining({
950+
body: { active_organization_id: 'org_123', intent: 'select_session' },
951+
method: 'POST',
952+
path: '/client/sessions/session_1/touch',
953+
}),
954+
expect.anything(),
955+
);
956+
});
905957
});
906958

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

packages/shared/src/types/session.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ export interface SessionResource extends ClerkResource {
240240
*/
241241
end: () => Promise<SessionResource>;
242242
remove: () => Promise<SessionResource>;
243-
touch: () => Promise<SessionResource>;
243+
touch: (params?: SessionTouchParams) => Promise<SessionResource>;
244244
getToken: GetToken;
245245
checkAuthorization: CheckAuthorization;
246246
clearCache: () => void;
@@ -262,7 +262,7 @@ export interface SessionResource extends ClerkResource {
262262
) => Promise<SessionVerificationResource>;
263263
verifyWithPasskey: () => Promise<SessionVerificationResource>;
264264
__internal_toSnapshot: () => SessionJSONSnapshot;
265-
__internal_touch: () => Promise<ClientResource | undefined>;
265+
__internal_touch: (params?: SessionTouchParams) => Promise<ClientResource | undefined>;
266266
}
267267

268268
/**
@@ -322,6 +322,12 @@ export type SessionStatus =
322322
| 'revoked'
323323
| 'pending';
324324

325+
export type SessionTouchIntent = 'focus' | 'select_session' | 'select_org';
326+
327+
export type SessionTouchParams = {
328+
intent?: SessionTouchIntent;
329+
};
330+
325331
export interface PublicUserData {
326332
firstName: string | null;
327333
lastName: string | null;

0 commit comments

Comments
 (0)