@@ -5,40 +5,44 @@ type TtlInMilliseconds = number;
55const DEFAULT_CACHE_TTL_MS = 86400000 ; // 24 hours
66
77/**
8- * Manages throttling for telemetry events using the browser's localStorage to
9- * mitigate event flooding in frequently executed code paths.
8+ * Interface for cache storage used by the telemetry throttler.
9+ * Implementations can use localStorage, in-memory storage, or any other storage mechanism.
10+ */
11+ export interface ThrottlerCache {
12+ getItem ( key : string ) : TtlInMilliseconds | undefined ;
13+ setItem ( key : string , value : TtlInMilliseconds ) : void ;
14+ removeItem ( key : string ) : void ;
15+ }
16+
17+ /**
18+ * Manages throttling for telemetry events using a configurable cache implementation
19+ * to mitigate event flooding in frequently executed code paths.
1020 */
1121export class TelemetryEventThrottler {
12- #storageKey = 'clerk_telemetry_throttler' ;
22+ #cache: ThrottlerCache ;
1323 #cacheTtl = DEFAULT_CACHE_TTL_MS ;
1424
15- isEventThrottled ( payload : TelemetryEvent ) : boolean {
16- if ( ! this . #isValidBrowser) {
17- return false ;
18- }
25+ constructor ( cache : ThrottlerCache ) {
26+ this . #cache = cache ;
27+ }
1928
29+ isEventThrottled ( payload : TelemetryEvent ) : boolean {
2030 const now = Date . now ( ) ;
2131 const key = this . #generateKey( payload ) ;
22- const entry = this . #cache?. [ key ] ;
32+ const entry = this . #cache. getItem ( key ) ;
2333
2434 if ( ! entry ) {
25- const updatedCache = {
26- ...this . #cache,
27- [ key ] : now ,
28- } ;
29-
30- localStorage . setItem ( this . #storageKey, JSON . stringify ( updatedCache ) ) ;
35+ this . #cache. setItem ( key , now ) ;
36+ return false ;
3137 }
3238
33- const shouldInvalidate = entry && now - entry > this . #cacheTtl;
39+ const shouldInvalidate = now - entry > this . #cacheTtl;
3440 if ( shouldInvalidate ) {
35- const updatedCache = this . #cache;
36- delete updatedCache [ key ] ;
37-
38- localStorage . setItem ( this . #storageKey, JSON . stringify ( updatedCache ) ) ;
41+ this . #cache. setItem ( key , now ) ;
42+ return false ;
3943 }
4044
41- return ! ! entry ;
45+ return true ;
4246 }
4347
4448 /**
@@ -62,51 +66,85 @@ export class TelemetryEventThrottler {
6266 . map ( key => sanitizedEvent [ key ] ) ,
6367 ) ;
6468 }
69+ }
6570
66- get #cache( ) : Record < string , TtlInMilliseconds > | undefined {
67- const cacheString = localStorage . getItem ( this . #storageKey) ;
68-
69- if ( ! cacheString ) {
70- return { } ;
71- }
71+ /**
72+ * LocalStorage-based cache implementation for browser environments.
73+ */
74+ export class LocalStorageThrottlerCache implements ThrottlerCache {
75+ #storageKey = 'clerk_telemetry_throttler' ;
7276
73- return JSON . parse ( cacheString ) ;
77+ getItem ( key : string ) : TtlInMilliseconds | undefined {
78+ return this . #getCache( ) [ key ] ;
7479 }
7580
76- /**
77- * Checks if the browser's localStorage is supported and writable.
78- *
79- * If any of these operations fail, it indicates that localStorage is either
80- * not supported or not writable (e.g., in cases where the storage is full or
81- * the browser is in a privacy mode that restricts localStorage usage).
82- */
83- get #isValidBrowser( ) : boolean {
84- if ( typeof window === 'undefined' ) {
85- return false ;
86- }
87-
88- const storage = window . localStorage ;
89- if ( ! storage ) {
90- return false ;
91- }
92-
81+ setItem ( key : string , value : TtlInMilliseconds ) : void {
9382 try {
94- const testKey = 'test' ;
95- storage . setItem ( testKey , testKey ) ;
96- storage . removeItem ( testKey ) ;
97-
98- return true ;
83+ const cache = this . #getCache( ) ;
84+ cache [ key ] = value ;
85+ localStorage . setItem ( this . #storageKey, JSON . stringify ( cache ) ) ;
9986 } catch ( err : unknown ) {
10087 const isQuotaExceededError =
10188 err instanceof DOMException &&
10289 // Check error names for different browsers
10390 ( err . name === 'QuotaExceededError' || err . name === 'NS_ERROR_DOM_QUOTA_REACHED' ) ;
10491
105- if ( isQuotaExceededError && storage . length > 0 ) {
106- storage . removeItem ( this . #storageKey) ;
92+ if ( isQuotaExceededError && localStorage . length > 0 ) {
93+ // Clear our cache if quota exceeded
94+ localStorage . removeItem ( this . #storageKey) ;
10795 }
96+ }
97+ }
10898
109- return false ;
99+ removeItem ( key : string ) : void {
100+ try {
101+ const cache = this . #getCache( ) ;
102+ delete cache [ key ] ;
103+ localStorage . setItem ( this . #storageKey, JSON . stringify ( cache ) ) ;
104+ } catch {
105+ // Silently fail if we can't remove
106+ }
107+ }
108+
109+ #getCache( ) : Record < string , TtlInMilliseconds > {
110+ try {
111+ const cacheString = localStorage . getItem ( this . #storageKey) ;
112+ if ( ! cacheString ) {
113+ return { } ;
114+ }
115+ return JSON . parse ( cacheString ) ;
116+ } catch {
117+ return { } ;
110118 }
111119 }
120+
121+ static isSupported ( ) : boolean {
122+ return typeof window !== 'undefined' && ! ! window . localStorage ;
123+ }
124+ }
125+
126+ /**
127+ * In-memory cache implementation for non-browser environments (e.g., React Native).
128+ */
129+ export class InMemoryThrottlerCache implements ThrottlerCache {
130+ #cache: Map < string , TtlInMilliseconds > = new Map ( ) ;
131+ #maxSize = 10000 ; // Defensive limit to prevent memory issues
132+
133+ getItem ( key : string ) : TtlInMilliseconds | undefined {
134+ // Defensive: clear cache if it gets too large
135+ if ( this . #cache. size > this . #maxSize) {
136+ this . #cache. clear ( ) ;
137+ return undefined ;
138+ }
139+
140+ return this . #cache. get ( key ) ;
141+ }
142+
143+ setItem ( key : string , value : TtlInMilliseconds ) : void {
144+ this . #cache. set ( key , value ) ;
145+ }
146+
147+ removeItem ( key : string ) : void {
148+ this . #cache. delete ( key ) ;
149+ }
112150}
0 commit comments