Skip to content

Commit 03bb01f

Browse files
committed
refactor broadcast to be intitiated within tokenCache
1 parent 80de3da commit 03bb01f

File tree

5 files changed

+296
-134
lines changed

5 files changed

+296
-134
lines changed

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

Lines changed: 25 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -325,28 +325,24 @@ describe('SessionTokenCache', () => {
325325
});
326326

327327
describe('broadcast sending', () => {
328-
it('broadcasts when set() is called with metadata', () => {
328+
it('broadcasts automatically when token resolves with valid claims', async () => {
329+
const futureExp = Math.floor(Date.now() / 1000) + 3600;
329330
const tokenResolver = Promise.resolve({
330331
getRawString: () => mockJwt,
331-
jwt: { claims: { iat: 1675876730 } },
332+
jwt: { claims: { exp: futureExp, iat: 1675876730, sid: 'session_123' } },
332333
} as any);
333334

334-
SessionTokenCache.set(
335-
{
336-
tokenId: 'session_123',
337-
tokenResolver,
338-
},
339-
{
340-
organizationId: null,
341-
sessionId: 'session_123',
342-
template: undefined,
343-
tokenRaw: mockJwt,
344-
},
345-
);
335+
SessionTokenCache.set({
336+
tokenId: 'session_123',
337+
tokenResolver,
338+
});
339+
340+
// Wait for the token to resolve and broadcast to happen
341+
await tokenResolver;
346342

347343
expect(mockBroadcastChannel.postMessage).toHaveBeenCalledWith(
348344
expect.objectContaining({
349-
organizationId: null,
345+
organizationId: undefined,
350346
sessionId: 'session_123',
351347
template: undefined,
352348
tokenId: 'session_123',
@@ -356,38 +352,36 @@ describe('SessionTokenCache', () => {
356352
);
357353
});
358354

359-
it('does not broadcast when set() is called without metadata', () => {
355+
it('does not broadcast when token has no sid claim', async () => {
356+
const futureExp = Math.floor(Date.now() / 1000) + 3600;
360357
const tokenResolver = Promise.resolve({
361358
getRawString: () => mockJwt,
362-
jwt: { claims: { iat: 1675876730 } },
359+
jwt: { claims: { exp: futureExp, iat: 1675876730 } },
363360
} as any);
364361

365362
SessionTokenCache.set({
366363
tokenId: 'session_123',
367364
tokenResolver,
368365
});
369366

367+
await tokenResolver;
368+
370369
expect(mockBroadcastChannel.postMessage).not.toHaveBeenCalled();
371370
});
372371

373-
it('validates tokenId matches expected format before broadcasting', () => {
372+
it('validates tokenId matches expected format before broadcasting', async () => {
373+
const futureExp = Math.floor(Date.now() / 1000) + 3600;
374374
const tokenResolver = Promise.resolve({
375375
getRawString: () => mockJwt,
376-
jwt: { claims: { iat: 1675876730 } },
376+
jwt: { claims: { exp: futureExp, iat: 1675876730, sid: 'session_123' } },
377377
} as any);
378378

379-
SessionTokenCache.set(
380-
{
381-
tokenId: 'wrong-token-id',
382-
tokenResolver,
383-
},
384-
{
385-
sessionId: 'session_123',
386-
tokenRaw: mockJwt,
387-
template: undefined,
388-
organizationId: null,
389-
},
390-
);
379+
SessionTokenCache.set({
380+
tokenId: 'wrong-token-id',
381+
tokenResolver,
382+
});
383+
384+
await tokenResolver;
391385

392386
expect(mockBroadcastChannel.postMessage).not.toHaveBeenCalled();
393387
});

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

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import {
3232
serializePublicKeyCredentialAssertion,
3333
webAuthnGetCredential as webAuthnGetCredentialOnWindow,
3434
} from '@/utils/passkeys';
35-
import { buildTokenId } from '@/utils/tokenId';
35+
import { TokenId } from '@/utils/tokenId';
3636

3737
import { clerkInvalidStrategy, clerkMissingWebAuthnPublicKeyOptions } from '../errors';
3838
import { eventBus, events } from '../events';
@@ -145,7 +145,7 @@ export class Session extends BaseResource implements SessionResource {
145145
#getCacheId(template?: string, organizationId?: string | null) {
146146
const resolvedOrganizationId =
147147
typeof organizationId === 'undefined' ? this.lastActiveOrganizationId : organizationId;
148-
return buildTokenId(this.id, template, resolvedOrganizationId);
148+
return TokenId.build(this.id, template, resolvedOrganizationId);
149149
}
150150

151151
startVerification = async ({ level }: SessionVerifyCreateParams): Promise<SessionVerificationResource> => {
@@ -399,19 +399,6 @@ export class Session extends BaseResource implements SessionResource {
399399
return tokenResolver.then(token => {
400400
const tokenRaw = token.getRawString();
401401

402-
// Update cache with broadcast metadata now that we have the resolved token
403-
SessionTokenCache.set(
404-
{ tokenId, tokenResolver: Promise.resolve(token) },
405-
tokenRaw
406-
? {
407-
organizationId,
408-
sessionId: this.id,
409-
template,
410-
tokenRaw,
411-
}
412-
: undefined,
413-
);
414-
415402
if (shouldDispatchTokenUpdate) {
416403
eventBus.emit(events.TokenUpdate, { token });
417404

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

Lines changed: 46 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { TokenResource } from '@clerk/types';
22

33
import { debugLogger } from '@/utils/debug';
4-
import { buildTokenId } from '@/utils/tokenId';
4+
import { TokenId } from '@/utils/tokenId';
55

66
import { Token } from './resources/internal';
77

@@ -30,30 +30,6 @@ interface TokenCacheEntry extends TokenCacheKeyJSON {
3030
tokenResolver: Promise<TokenResource>;
3131
}
3232

33-
/**
34-
* Metadata broadcast to other tabs when a token is fetched or refreshed.
35-
* Contains session and organization context along with the raw token value.
36-
*/
37-
export interface TokenBroadcastMetadata {
38-
/**
39-
* Organization ID associated with the token, if any.
40-
* Nullable for tokens not scoped to an organization.
41-
*/
42-
organizationId?: string | null;
43-
/**
44-
* Session ID for which the token was issued.
45-
*/
46-
sessionId: string;
47-
/**
48-
* JWT template name used to generate the token, if applicable.
49-
*/
50-
template?: string;
51-
/**
52-
* Raw JWT token string.
53-
*/
54-
tokenRaw: string;
55-
}
56-
5733
type Seconds = number;
5834

5935
/**
@@ -90,13 +66,12 @@ export interface TokenCache {
9066
get(cacheKeyJSON: TokenCacheKeyJSON, leeway?: number): TokenCacheEntry | undefined;
9167

9268
/**
93-
* Stores a token entry in the cache and optionally broadcasts to other tabs.
69+
* Stores a token entry in the cache and broadcasts to other tabs when the token resolves.
9470
*
9571
* @param entry - TokenCacheEntry containing tokenId, tokenResolver, and optional audience
96-
* @param broadcast - Optional metadata to broadcast this token update to other tabs via BroadcastChannel
97-
* Side effects: Schedules automatic expiration cleanup, broadcasts to other tabs if metadata provided
72+
* Side effects: Schedules automatic expiration cleanup, broadcasts to other tabs when token resolves
9873
*/
99-
set(entry: TokenCacheEntry, broadcast?: TokenBroadcastMetadata): void;
74+
set(entry: TokenCacheEntry): void;
10075

10176
/**
10277
* Returns the current number of cached entries.
@@ -224,7 +199,7 @@ const MemoryTokenCache = (prefix = KEY_PREFIX): TokenCache => {
224199
* Validates token ID, parses JWT, and updates cache if token is newer than existing entry.
225200
*/
226201
const handleBroadcastMessage = async ({ data }: MessageEvent<SessionTokenEvent>) => {
227-
const expectedTokenId = buildTokenId(data.sessionId, data.template, data.organizationId);
202+
const expectedTokenId = TokenId.build(data.sessionId, data.template, data.organizationId);
228203
if (data.tokenId !== expectedTokenId) {
229204
debugLogger.warn(
230205
'Ignoring token broadcast with mismatched tokenId',
@@ -304,49 +279,16 @@ const MemoryTokenCache = (prefix = KEY_PREFIX): TokenCache => {
304279
});
305280
};
306281

307-
const set = (entry: TokenCacheEntry, broadcast?: TokenBroadcastMetadata) => {
308-
// Ensure BroadcastChannel is initialized so this tab can receive broadcasts
309-
const channel = ensureBroadcastChannel();
282+
const set = (entry: TokenCacheEntry) => {
283+
ensureBroadcastChannel();
310284

311285
setInternal(entry);
312-
313-
if (broadcast && channel) {
314-
const expectedTokenId = buildTokenId(broadcast.sessionId, broadcast.template, broadcast.organizationId);
315-
if (entry.tokenId !== expectedTokenId) {
316-
return;
317-
}
318-
319-
// Generate a unique trace ID for this broadcast
320-
const traceId = `bc_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
321-
322-
debugLogger.info(
323-
'Broadcasting token update to other tabs',
324-
{
325-
organizationId: broadcast.organizationId,
326-
sessionId: broadcast.sessionId,
327-
template: broadcast.template,
328-
tokenId: entry.tokenId,
329-
traceId,
330-
},
331-
'tokenCache',
332-
);
333-
334-
const message: SessionTokenEvent = {
335-
organizationId: broadcast.organizationId,
336-
sessionId: broadcast.sessionId,
337-
template: broadcast.template,
338-
tokenId: entry.tokenId,
339-
tokenRaw: broadcast.tokenRaw,
340-
traceId,
341-
};
342-
343-
channel.postMessage(message);
344-
}
345286
};
346287

347288
/**
348289
* Internal cache setter that stores an entry and schedules expiration cleanup.
349290
* Resolves the token promise to extract expiration claims and set a deletion timeout.
291+
* Automatically broadcasts to other tabs when the token resolves.
350292
*/
351293
const setInternal = (entry: TokenCacheEntry) => {
352294
const cacheKey = new TokenCacheKey(prefix, {
@@ -395,6 +337,44 @@ const MemoryTokenCache = (prefix = KEY_PREFIX): TokenCache => {
395337
if (typeof (timeoutId as any).unref === 'function') {
396338
(timeoutId as any).unref();
397339
}
340+
341+
const channel = broadcastChannel;
342+
if (channel) {
343+
const tokenRaw = newToken.getRawString();
344+
if (tokenRaw && claims.sid) {
345+
const sessionId = claims.sid;
346+
const organizationId = claims.org_id || (claims.o as any)?.id;
347+
const template = TokenId.extractTemplate(entry.tokenId, sessionId, organizationId);
348+
349+
const expectedTokenId = TokenId.build(sessionId, template, organizationId);
350+
if (entry.tokenId === expectedTokenId) {
351+
const traceId = `bc_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
352+
353+
debugLogger.info(
354+
'Broadcasting token update to other tabs',
355+
{
356+
organizationId,
357+
sessionId,
358+
template,
359+
tokenId: entry.tokenId,
360+
traceId,
361+
},
362+
'tokenCache',
363+
);
364+
365+
const message: SessionTokenEvent = {
366+
organizationId,
367+
sessionId,
368+
template,
369+
tokenId: entry.tokenId,
370+
tokenRaw,
371+
traceId,
372+
};
373+
374+
channel.postMessage(message);
375+
}
376+
}
377+
}
398378
})
399379
.catch(() => {
400380
deleteKey();

0 commit comments

Comments
 (0)