|
1 | 1 | // @ts-nocheck |
2 | 2 | import { NextRequest, NextResponse } from "next/server"; |
3 | | -import { supabaseAdmin } from "@/lib/supabase"; |
4 | | -import { toDateStr } from "@/lib/dateUtils"; |
5 | | -import { calculateCurrentStreak } from "@/lib/streak"; |
6 | | -import { |
7 | | - cacheGet, |
8 | | - cacheSet, |
9 | | - isMetricsCacheBypassed, |
10 | | -} from "@/lib/metrics-cache"; |
11 | | -import { |
12 | | - pruneExpiredLeaderboardCache, |
13 | | - pruneExpiredRateLimits, |
14 | | - type LeaderboardCacheEntry, |
15 | | - type RateLimitEntry, |
16 | | -} from "@/lib/leaderboard-cache"; |
| 3 | +import { cacheGet, isMetricsCacheBypassed } from "@/lib/metrics-cache"; |
| 4 | +import { pruneExpiredRateLimits, type RateLimitEntry } from "@/lib/leaderboard-cache"; |
17 | 5 | import { |
18 | 6 | getUpstashConfig, |
19 | 7 | upstashRateLimitFixedWindow, |
@@ -77,156 +65,6 @@ async function checkRateLimit( |
77 | 65 | return checkMemoryRateLimit(ip); |
78 | 66 | } |
79 | 67 |
|
80 | | -function isFresh(payload: LeaderboardPayload): boolean { |
81 | | - const generatedAt = Date.parse(payload.generatedAt); |
82 | | - if (!Number.isFinite(generatedAt)) { |
83 | | - return false; |
84 | | - } |
85 | | - return Date.now() - generatedAt < CACHE_REFRESH_SECONDS * 1000; |
86 | | -} |
87 | | - |
88 | | -async function mapWithConcurrency<T, R>( |
89 | | - items: T[], |
90 | | - concurrency: number, |
91 | | - mapper: (item: T, index: number) => Promise<R> |
92 | | -): Promise<R[]> { |
93 | | - const safeConcurrency = |
94 | | - Number.isFinite(concurrency) && concurrency > 0 ? Math.floor(concurrency) : 1; |
95 | | - const results: R[] = new Array(items.length); |
96 | | - let cursor = 0; |
97 | | - |
98 | | - async function worker() { |
99 | | - while (true) { |
100 | | - const index = cursor; |
101 | | - cursor += 1; |
102 | | - if (index >= items.length) { |
103 | | - return; |
104 | | - } |
105 | | - results[index] = await mapper(items[index], index); |
106 | | - } |
107 | | - } |
108 | | - |
109 | | - const workers = Array.from( |
110 | | - { length: Math.min(safeConcurrency, items.length) }, |
111 | | - () => worker() |
112 | | - ); |
113 | | - |
114 | | - await Promise.all(workers); |
115 | | - return results; |
116 | | -} |
117 | | - |
118 | | -async function fetchGitHubJson<T>(path: string): Promise<T | null> { |
119 | | - const token = process.env.GITHUB_TOKEN; |
120 | | - const headers: Record<string, string> = { |
121 | | - Accept: "application/vnd.github+json", |
122 | | - }; |
123 | | - if (token) { |
124 | | - headers.Authorization = `Bearer ${token}`; |
125 | | - } |
126 | | - |
127 | | - const res = await fetch(`${GITHUB_API}${path}`, { |
128 | | - headers, |
129 | | - next: { revalidate: 3600 }, |
130 | | - }); |
131 | | - |
132 | | - if (!res.ok) { |
133 | | - console.error("GitHub leaderboard request failed:", path, res.status); |
134 | | - return null; |
135 | | - } |
136 | | - |
137 | | - return (await res.json()) as T; |
138 | | -} |
139 | | - |
140 | | - |
141 | | -async function fetchCommitStats(username: string, since: string) { |
142 | | - const query = new URLSearchParams({ |
143 | | - q: `author:${username} author-date:>=${since}`, |
144 | | - per_page: "100", |
145 | | - sort: "author-date", |
146 | | - order: "desc", |
147 | | - }); |
148 | | - return fetchGitHubJson<{ |
149 | | - total_count: number; |
150 | | - items: Array<{ commit: { author: { date: string } } }>; |
151 | | - }>(`/search/commits?${query.toString()}`); |
152 | | -} |
153 | | - |
154 | | -async function fetchPrCount(username: string, since: string): Promise<number> { |
155 | | - const query = new URLSearchParams({ |
156 | | - q: `author:${username} type:pr created:>=${since}`, |
157 | | - per_page: "1", |
158 | | - }); |
159 | | - const data = await fetchGitHubJson<{ total_count: number }>( |
160 | | - `/search/issues?${query.toString()}` |
161 | | - ); |
162 | | - return data?.total_count ?? 0; |
163 | | -} |
164 | | - |
165 | | -async function buildLeaderboard(): Promise<LeaderboardPayload> { |
166 | | - const { data: users, error } = await supabaseAdmin |
167 | | - .from("users") |
168 | | - .select("id, github_login") |
169 | | - .eq("is_public", true) |
170 | | - .eq("leaderboard_opt_in", true) |
171 | | - .limit(50); |
172 | | - |
173 | | - if (error) { |
174 | | - console.error("Failed to fetch leaderboard users:", error); |
175 | | - throw new Error("Failed to load leaderboard users"); |
176 | | - } |
177 | | - |
178 | | - const now = new Date(); |
179 | | - const monthStart = toDateStr(new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1))); |
180 | | - const streakStart = toDateStr(new Date(Date.now() - 90 * 86400000)); |
181 | | - |
182 | | - const safeUsers = (users ?? []) as PublicUser[]; |
183 | | - |
184 | | - const rows = await mapWithConcurrency( |
185 | | - safeUsers, |
186 | | - USER_CONCURRENCY, |
187 | | - async (user) => { |
188 | | - const [monthlyCommits, streakCommits, prs] = await Promise.all([ |
189 | | - fetchCommitStats(user.github_login, monthStart), |
190 | | - fetchCommitStats(user.github_login, streakStart), |
191 | | - fetchPrCount(user.github_login, monthStart), |
192 | | - ]); |
193 | | - |
194 | | - const streak = calculateCurrentStreak( |
195 | | - streakCommits?.items.map((item) => item.commit.author.date) ?? [] |
196 | | - ); |
197 | | - const commits = monthlyCommits?.total_count ?? 0; |
198 | | - const score = streak * 5 + commits + prs * 3; |
199 | | - |
200 | | - return { |
201 | | - rank: 0, |
202 | | - username: user.github_login, |
203 | | - avatarUrl: `https://github.com/${user.github_login}.png?size=96`, |
204 | | - profileUrl: `/u/${user.github_login}`, |
205 | | - streak, |
206 | | - commits, |
207 | | - prs, |
208 | | - score, |
209 | | - }; |
210 | | - } |
211 | | - ); |
212 | | - |
213 | | - const rankBy = (metric: LeaderboardMetric) => |
214 | | - [...rows] |
215 | | - .sort((a, b) => b[metric] - a[metric] || b.score - a.score) |
216 | | - .slice(0, 50) |
217 | | - .map((entry, index) => ({ ...entry, rank: index + 1 })); |
218 | | - |
219 | | - return { |
220 | | - generatedAt: now.toISOString(), |
221 | | - refreshSeconds: CACHE_REFRESH_SECONDS, |
222 | | - leaders: { |
223 | | - streak: rankBy("streak"), |
224 | | - commits: rankBy("commits"), |
225 | | - prs: rankBy("prs"), |
226 | | - }, |
227 | | - }; |
228 | | -} |
229 | | - |
230 | 68 | export async function GET(req: NextRequest) { |
231 | 69 | const ip = getRateLimitKey(req); |
232 | 70 | const rateLimit = await checkRateLimit(ip); |
|
0 commit comments