@@ -20,6 +20,139 @@ import { UserOutlined } from '@ant-design/icons';
2020import { CreatorButton } from './avatar/SubscriberAvatar.styles' ;
2121const { Text } = Typography ;
2222
23+ // LRU Cache implementation for profile caching
24+ interface CachedProfile {
25+ profile : SubscriberProfile ;
26+ timestamp : number ;
27+ accessCount : number ;
28+ lastAccessed : number ;
29+ }
30+
31+ const PROFILE_CACHE_DURATION = 600000 ; // 10 minutes in milliseconds
32+ const MAX_CACHE_SIZE = 5000 ; // Maximum number of cached profiles
33+ const CLEANUP_INTERVAL = 300000 ; // Clean up every 5 minutes
34+ const MAX_REQUEST_CACHE_SIZE = 100 ; // Maximum concurrent requests
35+
36+ class ProfileCache {
37+ private cache = new Map < string , CachedProfile > ( ) ;
38+ private requestCache = new Map < string , Promise < SubscriberProfile > > ( ) ;
39+ private cleanupTimer : NodeJS . Timeout | null = null ;
40+
41+ constructor ( ) {
42+ this . startCleanupTimer ( ) ;
43+ }
44+
45+ private startCleanupTimer ( ) : void {
46+ this . cleanupTimer = setInterval ( ( ) => {
47+ this . cleanup ( ) ;
48+ } , CLEANUP_INTERVAL ) ;
49+ }
50+
51+ private cleanup ( ) : void {
52+ const now = Date . now ( ) ;
53+ const expiredKeys : string [ ] = [ ] ;
54+
55+ // Find expired entries - convert to array first to avoid iterator issues
56+ const cacheEntries = Array . from ( this . cache . entries ( ) ) ;
57+ for ( const [ key , cached ] of cacheEntries ) {
58+ if ( now - cached . timestamp > PROFILE_CACHE_DURATION ) {
59+ expiredKeys . push ( key ) ;
60+ }
61+ }
62+
63+ // Remove expired entries
64+ expiredKeys . forEach ( key => this . cache . delete ( key ) ) ;
65+
66+ // If still over capacity, remove least recently used entries
67+ if ( this . cache . size > MAX_CACHE_SIZE ) {
68+ const entries = Array . from ( this . cache . entries ( ) ) ;
69+ entries . sort ( ( a , b ) => a [ 1 ] . lastAccessed - b [ 1 ] . lastAccessed ) ;
70+
71+ const toRemove = entries . slice ( 0 , this . cache . size - MAX_CACHE_SIZE ) ;
72+ toRemove . forEach ( ( [ key ] ) => this . cache . delete ( key ) ) ;
73+ }
74+
75+ // Cleanup request cache if it gets too large
76+ if ( this . requestCache . size > MAX_REQUEST_CACHE_SIZE ) {
77+ this . requestCache . clear ( ) ;
78+ }
79+
80+ }
81+
82+ getCachedProfile ( pubkey : string ) : SubscriberProfile | null {
83+ const cached = this . cache . get ( pubkey ) ;
84+ if ( ! cached ) return null ;
85+
86+ const isExpired = Date . now ( ) - cached . timestamp > PROFILE_CACHE_DURATION ;
87+ if ( isExpired ) {
88+ this . cache . delete ( pubkey ) ;
89+ return null ;
90+ }
91+
92+ // Update access statistics
93+ cached . accessCount ++ ;
94+ cached . lastAccessed = Date . now ( ) ;
95+
96+ return cached . profile ;
97+ }
98+
99+ setCachedProfile ( pubkey : string , profile : SubscriberProfile ) : void {
100+ const now = Date . now ( ) ;
101+ this . cache . set ( pubkey , {
102+ profile,
103+ timestamp : now ,
104+ accessCount : 1 ,
105+ lastAccessed : now
106+ } ) ;
107+
108+ // Trigger cleanup if cache is getting too large
109+ if ( this . cache . size > MAX_CACHE_SIZE * 1.1 ) {
110+ this . cleanup ( ) ;
111+ }
112+ }
113+
114+ getRequestPromise ( pubkey : string ) : Promise < SubscriberProfile > | null {
115+ return this . requestCache . get ( pubkey ) || null ;
116+ }
117+
118+ setRequestPromise ( pubkey : string , promise : Promise < SubscriberProfile > ) : void {
119+ this . requestCache . set ( pubkey , promise ) ;
120+
121+ // Clean up when promise completes
122+ promise . finally ( ( ) => {
123+ this . requestCache . delete ( pubkey ) ;
124+ } ) ;
125+ }
126+
127+ getCacheStats ( ) : { size : number ; requestCacheSize : number } {
128+ return {
129+ size : this . cache . size ,
130+ requestCacheSize : this . requestCache . size
131+ } ;
132+ }
133+
134+ destroy ( ) : void {
135+ if ( this . cleanupTimer ) {
136+ clearInterval ( this . cleanupTimer ) ;
137+ this . cleanupTimer = null ;
138+ }
139+ this . cache . clear ( ) ;
140+ this . requestCache . clear ( ) ;
141+ }
142+ }
143+
144+ // Global profile cache instance
145+ const globalProfileCache = new ProfileCache ( ) ;
146+
147+ // Helper functions for backward compatibility
148+ const getCachedProfile = ( pubkey : string ) : SubscriberProfile | null => {
149+ return globalProfileCache . getCachedProfile ( pubkey ) ;
150+ } ;
151+
152+ const setCachedProfile = ( pubkey : string , profile : SubscriberProfile ) : void => {
153+ globalProfileCache . setCachedProfile ( pubkey , profile ) ;
154+ } ;
155+
23156export const PaidSubscribers : React . FC = ( ) => {
24157 const hookResult = usePaidSubscribers ( 12 ) ;
25158 const { subscribers, fetchMore, hasMore, loading, useDummyData } = hookResult ;
@@ -31,7 +164,6 @@ export const PaidSubscribers: React.FC = () => {
31164
32165 // Modal state for view all subscribers
33166 const [ isViewAllModalVisible , setIsViewAllModalVisible ] = useState ( false ) ;
34- const [ allSubscribers , setAllSubscribers ] = useState < SubscriberProfile [ ] > ( [ ] ) ;
35167 const [ loadingProfiles , setLoadingProfiles ] = useState ( true ) ;
36168
37169 const [ subscriberProfiles , setSubscriberProfiles ] = useState < Map < string , SubscriberProfile > > (
@@ -68,11 +200,12 @@ export const PaidSubscribers: React.FC = () => {
68200 newMap . set ( pubkey , profile ) ;
69201 return newMap ;
70202 } ) ;
203+ // Cache the profile globally
204+ setCachedProfile ( pubkey , profile ) ;
71205 } ;
72206 // Handle opening view all modal
73207 const handleViewAll = async ( ) => {
74208 setIsViewAllModalVisible ( true ) ;
75- setAllSubscribers ( [ ...subscribers ] ) ; // Start with current subscribers
76209
77210 // Fetch more subscribers if available
78211 let canFetchMore = hasMore ;
@@ -90,23 +223,77 @@ export const PaidSubscribers: React.FC = () => {
90223 } ;
91224
92225 useEffect ( ( ) => {
93- // Implement hybrid profile fetching: NDK first, fallback to backend data
226+ // Implement hybrid profile fetching with 10-minute caching
94227 if ( useDummyData ) {
95228 setLoadingProfiles ( false ) ;
96229 return ;
97230 }
98231
99- const fetchProfiles = async ( ) => {
100- if ( ! ndkInstance || ! ndkInstance . ndk ) {
101- setLoadingProfiles ( false ) ;
102- return ;
232+ const fetchSingleProfile = async ( subscriber : SubscriberProfile ) : Promise < SubscriberProfile > => {
233+ // Check if we already have a cached profile that's still valid
234+ const cachedProfile = getCachedProfile ( subscriber . pubkey ) ;
235+ if ( cachedProfile ) {
236+ return cachedProfile ;
103237 }
104238
239+ // Check if there's already a request in progress for this profile
240+ const existingRequest = globalProfileCache . getRequestPromise ( subscriber . pubkey ) ;
241+ if ( existingRequest ) {
242+ return existingRequest ;
243+ }
105244
106- // Process each subscriber with hybrid approach
245+ // Create new request
246+ const profileRequest = ( async ( ) : Promise < SubscriberProfile > => {
247+ try {
248+
249+ if ( ! ndkInstance || ! ndkInstance . ndk ) {
250+ // No NDK available, return backend data
251+ return {
252+ ...subscriber ,
253+ name : subscriber . name || 'Anonymous Subscriber' ,
254+ picture : subscriber . picture || '' ,
255+ about : subscriber . about || ''
256+ } ;
257+ }
258+
259+ // Try to fetch profile from NDK (user's relay + other relays)
260+ const user = await ndkInstance . ndk ?. getUser ( { pubkey : subscriber . pubkey } ) . fetchProfile ( ) ;
261+
262+ if ( user && ( user . name || user . picture || user . about ) ) {
263+ // NDK returned a profile - use it as the primary source
264+ const ndkProfile = convertNDKUserProfileToSubscriberProfile ( subscriber . pubkey , user ) ;
265+ return ndkProfile ;
266+ } else {
267+ // NDK came up empty - fallback to backend data
268+ return {
269+ ...subscriber ,
270+ name : subscriber . name || 'Anonymous Subscriber' ,
271+ picture : subscriber . picture || '' ,
272+ about : subscriber . about || ''
273+ } ;
274+ }
275+ } catch ( error ) {
276+ // Error occurred - fallback to backend data
277+ return {
278+ ...subscriber ,
279+ name : subscriber . name || 'Anonymous Subscriber' ,
280+ picture : subscriber . picture || '' ,
281+ about : subscriber . about || ''
282+ } ;
283+ }
284+ } ) ( ) ;
285+
286+ // Store the promise in cache
287+ globalProfileCache . setRequestPromise ( subscriber . pubkey , profileRequest ) ;
288+
289+ return profileRequest ;
290+ } ;
291+
292+ const fetchProfiles = async ( ) => {
293+ // Process each subscriber with cached hybrid approach
107294 await Promise . all (
108295 subscribers . map ( async ( subscriber ) => {
109- // Skip if we already have a complete profile in our map
296+ // Skip if we already have a complete profile in our local map
110297 const existingProfile = subscriberProfiles . get ( subscriber . pubkey ) ;
111298 const hasValidProfile = existingProfile && (
112299 ( existingProfile . name && existingProfile . name !== 'Anonymous Subscriber' ) ||
@@ -119,30 +306,10 @@ export const PaidSubscribers: React.FC = () => {
119306 }
120307
121308 try {
122-
123- // Try to fetch profile from NDK (user's relay + other relays)
124- const user = await ndkInstance . ndk ?. getUser ( { pubkey : subscriber . pubkey } ) . fetchProfile ( ) ;
125-
126- if ( user && ( user . name || user . picture || user . about ) ) {
127- // NDK returned a profile - use it as the primary source
128-
129- const ndkProfile = convertNDKUserProfileToSubscriberProfile ( subscriber . pubkey , user ) ;
130- updateSubscriberProfile ( subscriber . pubkey , ndkProfile ) ;
131- } else {
132- // NDK came up empty - fallback to backend data
133-
134- // Use the backend data as-is since NDK had no better information
135- updateSubscriberProfile ( subscriber . pubkey , {
136- ...subscriber ,
137- // Ensure we have fallback values if backend data is also incomplete
138- name : subscriber . name || 'Anonymous Subscriber' ,
139- picture : subscriber . picture || '' ,
140- about : subscriber . about || ''
141- } ) ;
142- }
309+ const profile = await fetchSingleProfile ( subscriber ) ;
310+ updateSubscriberProfile ( subscriber . pubkey , profile ) ;
143311 } catch ( error ) {
144-
145- // Error occurred - fallback to backend data
312+ // Use fallback profile
146313 updateSubscriberProfile ( subscriber . pubkey , {
147314 ...subscriber ,
148315 name : subscriber . name || 'Anonymous Subscriber' ,
@@ -157,7 +324,7 @@ export const PaidSubscribers: React.FC = () => {
157324 } ;
158325
159326 fetchProfiles ( ) ;
160- } , [ subscribers , ndkInstance ] ) ;
327+ } , [ subscribers , ndkInstance , useDummyData , subscriberProfiles ] ) ;
161328
162329 // Handle closing view all modal
163330 const handleCloseViewAllModal = ( ) => {
0 commit comments