1- // src/lib/statsProvider.tsx
21
32/** @jsxImportSource react */
43import React , {
@@ -66,7 +65,9 @@ interface CommunityStatsProviderProps {
6665
6766const GITHUB_ORG = "recodehive" ;
6867const POINTS_PER_PR = 10 ;
69- const MAX_CONCURRENT_REQUESTS = 5 ; // Limit concurrent requests to avoid rate limiting
68+ const MAX_CONCURRENT_REQUESTS = 8 ; // Increased for better performance
69+ const CACHE_DURATION = 5 * 60 * 1000 ; // 5 minutes cache
70+ const MAX_PAGES_PER_REPO = 20 ; // Limit pages to prevent infinite loops on huge repos
7071
7172export function CommunityStatsProvider ( { children } : CommunityStatsProviderProps ) {
7273 const {
@@ -86,6 +87,12 @@ export function CommunityStatsProvider({ children }: CommunityStatsProviderProps
8687 // New state for leaderboard data
8788 const [ contributors , setContributors ] = useState < Contributor [ ] > ( [ ] ) ;
8889 const [ stats , setStats ] = useState < Stats | null > ( null ) ;
90+
91+ // Cache state
92+ const [ cache , setCache ] = useState < {
93+ data : { contributors : Contributor [ ] ; stats : Stats } | null ;
94+ timestamp : number ;
95+ } > ( { data : null , timestamp : 0 } ) ;
8996
9097 const fetchAllOrgRepos = useCallback ( async ( headers : Record < string , string > ) => {
9198 const repos : any [ ] = [ ] ;
@@ -108,28 +115,60 @@ export function CommunityStatsProvider({ children }: CommunityStatsProviderProps
108115 const fetchMergedPRsForRepo = useCallback ( async ( repoName : string , headers : Record < string , string > ) => {
109116 const mergedPRs : PullRequestItem [ ] = [ ] ;
110117 let page = 1 ;
111- while ( true ) {
112- const resp = await fetch (
113- `https://api.github.com/repos/${ GITHUB_ORG } /${ repoName } /pulls?state=closed&per_page=100&page=${ page } ` ,
114- { headers }
118+
119+ // Create promises for parallel pagination
120+ const pagePromises : Promise < PullRequestItem [ ] > [ ] = [ ] ;
121+
122+ // First, get the first page to estimate total pages
123+ const firstResp = await fetch (
124+ `https://api.github.com/repos/${ GITHUB_ORG } /${ repoName } /pulls?state=closed&per_page=100&page=1` ,
125+ { headers }
126+ ) ;
127+
128+ if ( ! firstResp . ok ) {
129+ console . warn ( `Failed to fetch PRs for ${ repoName } : ${ firstResp . status } ${ firstResp . statusText } ` ) ;
130+ return [ ] ;
131+ }
132+
133+ const firstPRs : PullRequestItem [ ] = await firstResp . json ( ) ;
134+ if ( ! Array . isArray ( firstPRs ) || firstPRs . length === 0 ) return [ ] ;
135+
136+ const firstPageMerged = firstPRs . filter ( ( pr ) => Boolean ( pr . merged_at ) ) ;
137+ mergedPRs . push ( ...firstPageMerged ) ;
138+
139+ // If we got less than 100, that's all there is
140+ if ( firstPRs . length < 100 ) return mergedPRs ;
141+
142+ // Estimate remaining pages (with a reasonable limit)
143+ const maxPages = Math . min ( MAX_PAGES_PER_REPO , 10 ) ; // Conservative estimate
144+
145+ // Create parallel requests for remaining pages
146+ for ( let i = 2 ; i <= maxPages ; i ++ ) {
147+ pagePromises . push (
148+ fetch (
149+ `https://api.github.com/repos/${ GITHUB_ORG } /${ repoName } /pulls?state=closed&per_page=100&page=${ i } ` ,
150+ { headers }
151+ )
152+ . then ( async ( resp ) => {
153+ if ( ! resp . ok ) return [ ] ;
154+ const prs : PullRequestItem [ ] = await resp . json ( ) ;
155+ if ( ! Array . isArray ( prs ) ) return [ ] ;
156+ return prs . filter ( ( pr ) => Boolean ( pr . merged_at ) ) ;
157+ } )
158+ . catch ( ( ) => [ ] )
115159 ) ;
116- if ( ! resp . ok ) {
117- console . warn ( `Failed to fetch PRs for ${ repoName } : ${ resp . status } ${ resp . statusText } ` ) ;
118- break ;
119- }
120- const prs : PullRequestItem [ ] = await resp . json ( ) ;
121- if ( ! Array . isArray ( prs ) || prs . length === 0 ) break ;
122-
123- const merged = prs . filter ( ( pr ) => Boolean ( pr . merged_at ) ) ;
124- mergedPRs . push ( ...merged ) ;
125-
126- if ( prs . length < 100 ) break ;
127- page ++ ;
128160 }
161+
162+ // Wait for all pages in parallel
163+ const remainingPages = await Promise . all ( pagePromises ) ;
164+ remainingPages . forEach ( pagePRs => {
165+ if ( pagePRs . length > 0 ) mergedPRs . push ( ...pagePRs ) ;
166+ } ) ;
167+
129168 return mergedPRs ;
130169 } , [ ] ) ;
131170
132- // NEW: Concurrent processing function with controlled concurrency
171+ // Concurrent processing function with controlled concurrency
133172 const processBatch = useCallback ( async (
134173 repos : any [ ] ,
135174 headers : Record < string , string >
@@ -184,6 +223,17 @@ export function CommunityStatsProvider({ children }: CommunityStatsProviderProps
184223 const fetchAllStats = useCallback ( async ( signal : AbortSignal ) => {
185224 setLoading ( true ) ;
186225 setError ( null ) ;
226+
227+ // Check cache first
228+ const now = Date . now ( ) ;
229+ if ( cache . data && ( now - cache . timestamp ) < CACHE_DURATION ) {
230+ // console.log('Using cached leaderboard data');
231+ setContributors ( cache . data . contributors ) ;
232+ setStats ( cache . data . stats ) ;
233+ setLoading ( false ) ;
234+ return ;
235+ }
236+
187237 if ( ! token ) {
188238 setError ( "GitHub token not found. Please set customFields.gitToken in docusaurus.config.js." ) ;
189239 setLoading ( false ) ;
@@ -196,29 +246,40 @@ export function CommunityStatsProvider({ children }: CommunityStatsProviderProps
196246 Accept : "application/vnd.github.v3+json" ,
197247 } ;
198248
199- // Fetch general organization stats (unchanged)
200- const orgStats : GitHubOrgStats = await githubService . fetchOrganizationStats ( signal ) ;
249+ // Fetch both org stats and repos in parallel
250+ const [ orgStats , repos ] = await Promise . all ( [
251+ githubService . fetchOrganizationStats ( signal ) ,
252+ fetchAllOrgRepos ( headers )
253+ ] ) ;
254+
255+ // Set org stats immediately
201256 setGithubStarCount ( orgStats . totalStars ) ;
202257 setGithubContributorsCount ( orgStats . totalContributors ) ;
203258 setGithubForksCount ( orgStats . totalForks ) ;
204259 setGithubReposCount ( orgStats . publicRepositories ) ;
205260 setGithubDiscussionsCount ( orgStats . discussionsCount ) ;
206261 setLastUpdated ( new Date ( orgStats . lastUpdated ) ) ;
207262
208- // Fetch leaderboard data with concurrent processing
209- const repos = await fetchAllOrgRepos ( headers ) ;
210-
211- // NEW: Use concurrent processing instead of sequential
263+ // Process leaderboard data with concurrent processing
212264 const { contributorMap, totalMergedPRs } = await processBatch ( repos , headers ) ;
213265
214266 const sortedContributors = Array . from ( contributorMap . values ( ) ) . sort (
215267 ( a , b ) => b . points - a . points || b . prs - a . prs
216268 ) ;
217- setContributors ( sortedContributors ) ;
218- setStats ( {
269+
270+ const statsData = {
219271 flooredTotalPRs : totalMergedPRs ,
220272 totalContributors : sortedContributors . length ,
221273 flooredTotalPoints : sortedContributors . reduce ( ( sum , c ) => sum + c . points , 0 ) ,
274+ } ;
275+
276+ setContributors ( sortedContributors ) ;
277+ setStats ( statsData ) ;
278+
279+ // Cache the results
280+ setCache ( {
281+ data : { contributors : sortedContributors , stats : statsData } ,
282+ timestamp : now
222283 } ) ;
223284
224285 } catch ( err : any ) {
@@ -236,12 +297,13 @@ export function CommunityStatsProvider({ children }: CommunityStatsProviderProps
236297 } finally {
237298 setLoading ( false ) ;
238299 }
239- } , [ token , fetchAllOrgRepos , processBatch ] ) ;
300+ } , [ token , fetchAllOrgRepos , processBatch , cache ] ) ;
240301
241302 const clearCache = useCallback ( ( ) => {
242303 githubService . clearCache ( ) ;
304+ setCache ( { data : null , timestamp : 0 } ) ; // Clear local cache too
243305 const abortController = new AbortController ( ) ;
244- fetchAllStats ( abortController . signal ) ;
306+ fetchAllStats ( abortController . signal ) ; // Refetch data after clearing cache
245307 } , [ fetchAllStats ] ) ;
246308
247309 useEffect ( ( ) => {
@@ -296,16 +358,16 @@ export const useCommunityStatsContext = (): ICommunityStatsContext => {
296358
297359export const convertStatToText = ( num : number ) : string => {
298360 const hasIntlSupport =
299- typeof Intl === "object" && Intl && typeof Intl . NumberFormat === "function" ;
361+ typeof Intl === "object" && Intl && typeof Intl . NumberFormat === "function" ;
300362
301363 if ( ! hasIntlSupport ) {
302- return `${ ( num / 1000 ) . toFixed ( 1 ) } k` ;
364+ return `${ ( num / 1000 ) . toFixed ( 1 ) } k` ; // Fallback for environments without Intl support
303365 }
304366
305367 const formatter = new Intl . NumberFormat ( "en-US" , {
306- notation : "compact" ,
368+ notation : "compact" ,
307369 compactDisplay : "short" ,
308- maximumSignificantDigits : 3 ,
370+ maximumSignificantDigits : 3 , // More precise formatting
309371 } ) ;
310372 return formatter . format ( num ) ;
311373} ;
0 commit comments