Skip to content

Commit ab8ea9d

Browse files
authored
Merge branch 'main' into aa/fix-typedoc
2 parents 54347c2 + f47b5a3 commit ab8ea9d

File tree

7 files changed

+185
-27
lines changed

7 files changed

+185
-27
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

.changeset/silly-jars-shave.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
---
2+
---

.typedoc/custom-theme.mjs

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ class ClerkMarkdownThemeContext extends MarkdownThemeContext {
4444

4545
const superPartials = this.partials;
4646

47+
this._insideFunctionSignature = false;
48+
4749
this.partials = {
4850
...superPartials,
4951
/**
@@ -153,10 +155,17 @@ class ClerkMarkdownThemeContext extends MarkdownThemeContext {
153155
);
154156
}
155157

158+
const prevInsideParams = this._insideFunctionSignature;
159+
this._insideFunctionSignature = true;
156160
md.push(this.partials.signatureParameters(model.parameters || []));
161+
this._insideFunctionSignature = prevInsideParams;
157162

158163
if (model.type) {
159-
md.push(`: ${this.partials.someType(model.type)}`);
164+
const prevInsideType = this._insideFunctionSignature;
165+
this._insideFunctionSignature = true;
166+
const typeOutput = this.partials.someType(model.type);
167+
this._insideFunctionSignature = prevInsideType;
168+
md.push(`: ${typeOutput}`);
160169
}
161170

162171
const result = md.join('');
@@ -353,6 +362,11 @@ class ClerkMarkdownThemeContext extends MarkdownThemeContext {
353362
.replace(/<code>/g, '')
354363
.replace(/<\/code>/g, '');
355364

365+
// Only wrap in <code> if NOT inside a function signature
366+
if (this._insideFunctionSignature) {
367+
return output;
368+
}
369+
356370
return `<code>${output}</code>`;
357371
},
358372
/**
@@ -371,6 +385,11 @@ class ClerkMarkdownThemeContext extends MarkdownThemeContext {
371385
.replace(/<code>/g, '')
372386
.replace(/<\/code>/g, '');
373387

388+
// Only wrap in <code> if NOT inside a function signature
389+
if (this._insideFunctionSignature) {
390+
return output;
391+
}
392+
374393
return `<code>${output}</code>`;
375394
},
376395
/**
@@ -394,6 +413,11 @@ class ClerkMarkdownThemeContext extends MarkdownThemeContext {
394413
)
395414
.join(delimiter);
396415

416+
// Only wrap in <code> if NOT inside a function signature
417+
if (this._insideFunctionSignature) {
418+
return output;
419+
}
420+
397421
return `<code>${output}</code>`;
398422
},
399423
/**
@@ -492,6 +516,32 @@ ${tabs}
492516
.replace(/<code>/g, '')
493517
.replace(/<\/code>/g, '');
494518

519+
// Only wrap in <code> if NOT inside a function signature
520+
if (this._insideFunctionSignature) {
521+
return output;
522+
}
523+
524+
return `<code>${output}</code>`;
525+
},
526+
/**
527+
* Ensures that reflection types (like Simplify wrapped types) are wrapped in a single codeblock
528+
* @param {import('typedoc').ReflectionType} model
529+
*/
530+
reflectionType: model => {
531+
const defaultOutput = superPartials.reflectionType(model);
532+
533+
const output = defaultOutput
534+
// Remove any backticks
535+
.replace(/`/g, '')
536+
// Remove any `<code>` and `</code>` tags
537+
.replace(/<code>/g, '')
538+
.replace(/<\/code>/g, '');
539+
540+
// Only wrap in <code> if NOT inside a function signature
541+
if (this._insideFunctionSignature) {
542+
return output;
543+
}
544+
495545
return `<code>${output}</code>`;
496546
},
497547
/**

packages/backend/src/tokens/verify.ts

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -40,22 +40,11 @@ export type VerifyTokenOptions = Simplify<
4040
* Verifies a Clerk-generated token signature. Networkless if the `jwtKey` is provided. Otherwise, performs a network call to retrieve the JWKS from the [Backend API](https://clerk.com/docs/reference/backend-api/tag/JWKS#operation/GetJWKS){{ target: '_blank' }}.
4141
*
4242
* @param token - The token to verify.
43-
* @param options - Options for verifying the token.
43+
* @param options - Options for verifying the token. It is recommended to set these options as [environment variables](/docs/guides/development/clerk-environment-variables#api-and-sdk-configuration) where possible, and then pass them to the function. For example, you can set the `secretKey` option using the `CLERK_SECRET_KEY` environment variable, and then pass it to the function like this: `verifyToken(token, { secretKey: process.env.CLERK_SECRET_KEY })`.
4444
*
4545
* @displayFunctionSignature
4646
* @hideReturns
4747
*
48-
* @paramExtension
49-
*
50-
* ### `VerifyTokenOptions`
51-
*
52-
* It is recommended to set these options as [environment variables](/docs/guides/development/clerk-environment-variables#api-and-sdk-configuration) where possible, and then pass them to the function. For example, you can set the `secretKey` option using the `CLERK_SECRET_KEY` environment variable, and then pass it to the function like this: `createClerkClient({ secretKey: process.env.CLERK_SECRET_KEY })`.
53-
*
54-
* > [!WARNING]
55-
* You must provide either `jwtKey` or `secretKey`.
56-
*
57-
* <Typedoc src="backend/verify-token-options" />
58-
*
5948
* @example
6049
*
6150
* The following example demonstrates how to use the [JavaScript Backend SDK](https://clerk.com/docs/reference/backend/overview) to verify the token signature.

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)