Skip to content

Commit f47b5a3

Browse files
authored
feat(clerk-js): Update SessionTokenCache only if un-cached (#7105)
1 parent d75be61 commit f47b5a3

File tree

4 files changed

+131
-14
lines changed

4 files changed

+131
-14
lines changed

.changeset/modern-cars-fall.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/clerk-js': patch
3+
---
4+
5+
Optimize Session.#hydrateCache to only cache token if it's new/different

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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,41 @@ describe('SessionTokenCache', () => {
277277
// Critical: postMessage should NOT be called when handling a broadcast
278278
expect(mockBroadcastChannel.postMessage).not.toHaveBeenCalled();
279279
});
280+
281+
it('always broadcasts regardless of cache state', async () => {
282+
mockBroadcastChannel.postMessage.mockClear();
283+
284+
const tokenId = 'sess_2GbDB4enNdCa5vS1zpC3Xzg9tK9';
285+
const tokenResolver = Promise.resolve(
286+
new Token({
287+
id: tokenId,
288+
jwt: mockJwt,
289+
object: 'token',
290+
}) as TokenResource,
291+
);
292+
293+
SessionTokenCache.set({ tokenId, tokenResolver });
294+
await tokenResolver;
295+
296+
expect(mockBroadcastChannel.postMessage).toHaveBeenCalledTimes(1);
297+
const firstCall = mockBroadcastChannel.postMessage.mock.calls[0][0];
298+
expect(firstCall.tokenId).toBe(tokenId);
299+
300+
mockBroadcastChannel.postMessage.mockClear();
301+
302+
const tokenResolver2 = Promise.resolve(
303+
new Token({
304+
id: tokenId,
305+
jwt: mockJwt,
306+
object: 'token',
307+
}) as TokenResource,
308+
);
309+
310+
SessionTokenCache.set({ tokenId, tokenResolver: tokenResolver2 });
311+
await tokenResolver2;
312+
313+
expect(mockBroadcastChannel.postMessage).toHaveBeenCalledTimes(1);
314+
});
280315
});
281316

282317
describe('token expiration with absolute time', () => {

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,9 +131,15 @@ export class Session extends BaseResource implements SessionResource {
131131
};
132132

133133
#hydrateCache = (token: TokenResource | null) => {
134-
if (token) {
134+
if (!token) {
135+
return;
136+
}
137+
138+
const tokenId = this.#getCacheId();
139+
const existing = SessionTokenCache.get({ tokenId });
140+
if (!existing) {
135141
SessionTokenCache.set({
136-
tokenId: this.#getCacheId(),
142+
tokenId,
137143
tokenResolver: Promise.resolve(token),
138144
});
139145
}

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

Lines changed: 83 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ describe('Session', () => {
3535

3636
beforeEach(() => {
3737
dispatchSpy = vi.spyOn(eventBus, 'emit');
38-
BaseResource.clerk = clerkMock() as any;
38+
BaseResource.clerk = clerkMock();
3939
});
4040

4141
afterEach(() => {
@@ -76,7 +76,7 @@ describe('Session', () => {
7676
it('hydrates token cache from lastActiveToken', async () => {
7777
BaseResource.clerk = clerkMock({
7878
organization: new Organization({ id: 'activeOrganization' } as OrganizationJSON),
79-
}) as any;
79+
});
8080

8181
const session = new Session({
8282
status: 'active',
@@ -100,10 +100,81 @@ describe('Session', () => {
100100
expect(dispatchSpy).toHaveBeenCalledTimes(2);
101101
});
102102

103+
it('does not re-cache token when Session is reconstructed with same token', async () => {
104+
BaseResource.clerk = clerkMock({
105+
organization: new Organization({ id: 'activeOrganization' } as OrganizationJSON),
106+
});
107+
108+
SessionTokenCache.clear();
109+
110+
const session1 = new Session({
111+
status: 'active',
112+
id: 'session_1',
113+
object: 'session',
114+
user: createUser({}),
115+
last_active_organization_id: 'activeOrganization',
116+
last_active_token: { object: 'token', jwt: mockJwt },
117+
actor: null,
118+
created_at: new Date().getTime(),
119+
updated_at: new Date().getTime(),
120+
} as SessionJSON);
121+
122+
expect(SessionTokenCache.size()).toBe(1);
123+
const cachedEntry1 = SessionTokenCache.get({ tokenId: 'session_1-activeOrganization' });
124+
expect(cachedEntry1).toBeDefined();
125+
126+
const session2 = new Session({
127+
status: 'active',
128+
id: 'session_1',
129+
object: 'session',
130+
user: createUser({}),
131+
last_active_organization_id: 'activeOrganization',
132+
last_active_token: { object: 'token', jwt: mockJwt },
133+
actor: null,
134+
created_at: new Date().getTime(),
135+
updated_at: new Date().getTime(),
136+
} as SessionJSON);
137+
138+
expect(SessionTokenCache.size()).toBe(1);
139+
140+
const token1 = await session1.getToken();
141+
const token2 = await session2.getToken();
142+
143+
expect(token1).toBe(token2);
144+
expect(token1).toEqual(mockJwt);
145+
expect(BaseResource.clerk.getFapiClient().request).not.toHaveBeenCalled();
146+
});
147+
148+
it('caches token from cookie during degraded mode recovery', async () => {
149+
BaseResource.clerk = clerkMock();
150+
151+
SessionTokenCache.clear();
152+
153+
const sessionFromCookie = new Session({
154+
status: 'active',
155+
id: 'session_1',
156+
object: 'session',
157+
user: createUser({}),
158+
last_active_organization_id: null,
159+
last_active_token: { object: 'token', jwt: mockJwt },
160+
actor: null,
161+
created_at: new Date().getTime(),
162+
updated_at: new Date().getTime(),
163+
} as SessionJSON);
164+
165+
expect(SessionTokenCache.size()).toBe(1);
166+
const cachedEntry = SessionTokenCache.get({ tokenId: 'session_1' });
167+
expect(cachedEntry).toBeDefined();
168+
169+
const token = await sessionFromCookie.getToken();
170+
expect(token).toEqual(mockJwt);
171+
expect(BaseResource.clerk.getFapiClient().request).not.toHaveBeenCalled();
172+
});
173+
103174
it('dispatches token:update event on getToken with active organization', async () => {
104175
BaseResource.clerk = clerkMock({
105176
organization: new Organization({ id: 'activeOrganization' } as OrganizationJSON),
106-
}) as any;
177+
});
107178

108179
const session = new Session({
109180
status: 'active',
@@ -138,7 +209,7 @@ describe('Session', () => {
138209
it('does not dispatch token:update if template is provided', async () => {
139210
BaseResource.clerk = clerkMock({
140211
organization: new Organization({ id: 'activeOrganization' } as OrganizationJSON),
141-
}) as any;
212+
});
142213

143214
const session = new Session({
144215
status: 'active',
@@ -159,7 +230,7 @@ describe('Session', () => {
159230
it('dispatches token:update when provided organization ID matches current active organization', async () => {
160231
BaseResource.clerk = clerkMock({
161232
organization: new Organization({ id: 'activeOrganization' } as OrganizationJSON),
162-
}) as any;
233+
});
163234

164235
const session = new Session({
165236
status: 'active',
@@ -178,7 +249,7 @@ describe('Session', () => {
178249
});
179250

180251
it('does not dispatch token:update when provided organization ID does not match current active organization', async () => {
181-
BaseResource.clerk = clerkMock() as any;
252+
BaseResource.clerk = clerkMock();
182253

183254
const session = new Session({
184255
status: 'active',
@@ -240,7 +311,7 @@ describe('Session', () => {
240311
it(`uses the current session's lastActiveOrganizationId by default, not clerk.organization.id`, async () => {
241312
BaseResource.clerk = clerkMock({
242313
organization: new Organization({ id: 'oldActiveOrganization' } as OrganizationJSON),
243-
}) as any;
314+
});
244315

245316
const session = new Session({
246317
status: 'active',
@@ -261,7 +332,7 @@ describe('Session', () => {
261332
});
262333

263334
it('deduplicates concurrent getToken calls to prevent multiple API requests', async () => {
264-
BaseResource.clerk = clerkMock() as any;
335+
BaseResource.clerk = clerkMock();
265336

266337
const session = new Session({
267338
status: 'active',
@@ -286,7 +357,7 @@ describe('Session', () => {
286357
});
287358

288359
it('deduplicates concurrent getToken calls with same template', async () => {
289-
BaseResource.clerk = clerkMock() as any;
360+
BaseResource.clerk = clerkMock();
290361

291362
const session = new Session({
292363
status: 'active',
@@ -313,7 +384,7 @@ describe('Session', () => {
313384
});
314385

315386
it('does not deduplicate getToken calls with different templates', async () => {
316-
BaseResource.clerk = clerkMock() as any;
387+
BaseResource.clerk = clerkMock();
317388

318389
const session = new Session({
319390
status: 'active',
@@ -335,7 +406,7 @@ describe('Session', () => {
335406
});
336407

337408
it('does not deduplicate getToken calls with different organization IDs', async () => {
338-
BaseResource.clerk = clerkMock() as any;
409+
BaseResource.clerk = clerkMock();
339410

340411
const session = new Session({
341412
status: 'active',
@@ -362,7 +433,7 @@ describe('Session', () => {
362433

363434
beforeEach(() => {
364435
dispatchSpy = vi.spyOn(eventBus, 'emit');
365-
BaseResource.clerk = clerkMock() as any;
436+
BaseResource.clerk = clerkMock();
366437
});
367438

368439
afterEach(() => {

0 commit comments

Comments
 (0)