Skip to content

Commit 4a6769d

Browse files
committed
Add lru map to prevent unbounded growth
1 parent c8c4648 commit 4a6769d

File tree

3 files changed

+110
-3
lines changed

3 files changed

+110
-3
lines changed

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { isWebAuthnSupported as isWebAuthnSupportedOnWindow } from '@clerk/share
2828

2929
import { unixEpochToDate } from '@/utils/date';
3030
import { debugLogger } from '@/utils/debug';
31+
import { LruMap } from '@/utils/lru-map';
3132
import {
3233
convertJSONToPublicKeyRequestOptions,
3334
serializePublicKeyCredentialAssertion,
@@ -46,13 +47,12 @@ import { SessionVerification } from './SessionVerification';
4647
* Cache of per-tokenId locks for cross-tab coordination.
4748
* Each unique tokenId gets its own lock, allowing different token types
4849
* (e.g., different orgs, JWT templates) to be fetched in parallel.
50+
* Uses LRU eviction to prevent unbounded growth.
4951
*/
50-
const tokenLocks = new Map<string, ReturnType<typeof SafeLock>>();
52+
const tokenLocks = new LruMap<string, ReturnType<typeof SafeLock>>(50);
5153

5254
/**
5355
* Gets or creates a cross-tab lock for a specific tokenId.
54-
* Using per-tokenId locks allows different token types to be fetched in parallel
55-
* while still preventing duplicate fetches for the same token across tabs.
5656
*/
5757
function getTokenLock(tokenId: string) {
5858
let lock = tokenLocks.get(tokenId);
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import { LruMap } from '../lru-map';
4+
5+
describe('LruMap', () => {
6+
it('stores and retrieves values', () => {
7+
const map = new LruMap<string, number>(3);
8+
map.set('a', 1);
9+
map.set('b', 2);
10+
11+
expect(map.get('a')).toBe(1);
12+
expect(map.get('b')).toBe(2);
13+
expect(map.get('c')).toBeUndefined();
14+
});
15+
16+
it('evicts oldest entry when exceeding max size', () => {
17+
const map = new LruMap<string, number>(3);
18+
map.set('a', 1);
19+
map.set('b', 2);
20+
map.set('c', 3);
21+
map.set('d', 4);
22+
23+
expect(map.get('a')).toBeUndefined();
24+
expect(map.get('b')).toBe(2);
25+
expect(map.get('c')).toBe(3);
26+
expect(map.get('d')).toBe(4);
27+
expect(map.size).toBe(3);
28+
});
29+
30+
it('moves accessed entry to most recent position', () => {
31+
const map = new LruMap<string, number>(3);
32+
map.set('a', 1);
33+
map.set('b', 2);
34+
map.set('c', 3);
35+
36+
map.get('a');
37+
38+
map.set('d', 4);
39+
40+
expect(map.get('a')).toBe(1);
41+
expect(map.get('b')).toBeUndefined();
42+
expect(map.get('c')).toBe(3);
43+
expect(map.get('d')).toBe(4);
44+
});
45+
46+
it('updates existing entry without eviction', () => {
47+
const map = new LruMap<string, number>(3);
48+
map.set('a', 1);
49+
map.set('b', 2);
50+
map.set('c', 3);
51+
52+
map.set('a', 100);
53+
54+
expect(map.get('a')).toBe(100);
55+
expect(map.size).toBe(3);
56+
57+
map.set('d', 4);
58+
59+
expect(map.get('a')).toBe(100);
60+
expect(map.get('b')).toBeUndefined();
61+
expect(map.size).toBe(3);
62+
});
63+
64+
it('handles max size of 1', () => {
65+
const map = new LruMap<string, number>(1);
66+
map.set('a', 1);
67+
map.set('b', 2);
68+
69+
expect(map.get('a')).toBeUndefined();
70+
expect(map.get('b')).toBe(2);
71+
expect(map.size).toBe(1);
72+
});
73+
});
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/**
2+
* A simple Map with LRU (Least Recently Used) eviction.
3+
* When the map exceeds maxSize, the oldest entries are removed.
4+
*/
5+
export class LruMap<K, V> extends Map<K, V> {
6+
constructor(private maxSize: number) {
7+
super();
8+
}
9+
10+
override get(key: K): V | undefined {
11+
const value = super.get(key);
12+
if (value !== undefined) {
13+
super.delete(key);
14+
super.set(key, value);
15+
}
16+
return value;
17+
}
18+
19+
override set(key: K, value: V): this {
20+
if (super.has(key)) {
21+
super.delete(key);
22+
} else {
23+
while (this.size >= this.maxSize) {
24+
const oldest = super.keys().next().value;
25+
if (oldest !== undefined) {
26+
super.delete(oldest);
27+
} else {
28+
break;
29+
}
30+
}
31+
}
32+
return super.set(key, value);
33+
}
34+
}

0 commit comments

Comments
 (0)