Skip to content

Commit 5204a7d

Browse files
authored
Merge pull request #524 from Adez017/blogs
Optimize the stats load with the use of caching
2 parents 88b5863 + 759e760 commit 5204a7d

File tree

1 file changed

+95
-33
lines changed

1 file changed

+95
-33
lines changed

src/lib/statsProvider.tsx

Lines changed: 95 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
// src/lib/statsProvider.tsx
21

32
/** @jsxImportSource react */
43
import React, {
@@ -66,7 +65,9 @@ interface CommunityStatsProviderProps {
6665

6766
const GITHUB_ORG = "recodehive";
6867
const 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

7172
export 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

297359
export 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

Comments
 (0)