Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/app/api/leaderboard/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,14 @@ export async function GET(request: Request) {

try {
const result = await getLeaderboardResult(country);
return NextResponse.json({ success: true, ...result });
return NextResponse.json(
{ success: true, ...result },
{
headers: {
"Cache-Control": "public, s-maxage=3600, stale-while-revalidate=86400",
},
},
);
} catch (err) {
console.error("Leaderboard DB query failed:", err);

Expand Down
9 changes: 8 additions & 1 deletion src/app/api/user/[username]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,14 @@ export async function GET(request: Request, { params }: { params: Promise<{ user

try {
const { user, location } = await getUserProfile(trimmed, selectedLanguages);
return NextResponse.json({ success: true, user, location });
return NextResponse.json(
{ success: true, user, location },
{
headers: {
"Cache-Control": "public, s-maxage=1800, stale-while-revalidate=86400",
},
},
);
} catch (error: unknown) {
console.error("User profile fetch error:", error);
return formatApiErrorResponse(error);
Expand Down
2 changes: 1 addition & 1 deletion src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export const metadata: Metadata = {
export default async function RootLayout({ children }: { children: ReactNode }) {
const cookieStore = await cookies();
const headerStore = await headers();
const cookieLocale = cookieStore.get(LOCALE_COOKIE)?.value;
const cookieLocale = cookieStore.get(LOCALE_COOKIE)?.value || headerStore.get("x-locale");
const initialLocale = isSupportedLocale(cookieLocale)
? cookieLocale
: parseAcceptLanguage(headerStore.get("accept-language"), supportedLocales, DEFAULT_LOCALE);
Expand Down
7 changes: 6 additions & 1 deletion src/app/leaderboard/[country]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,13 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
};
}

export const revalidate = 3600; // Cache on Vercel Edge CDN for 1 hour via ISR
export const dynamicParams = true;

export async function generateStaticParams() {
return countries.map((country) => ({ country: country.slug }));
// Pre-render the top 20 most visited countries at build time.
// Other countries are generated on-demand upon first visit and cached by ISR.
return countries.slice(0, 20).map((country) => ({ country: country.slug }));
}

export default async function CountryLeaderboardPage({ params }: Props) {
Expand Down
5 changes: 4 additions & 1 deletion src/app/scoring-methodology/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Suspense } from "react";
import type { Metadata } from "next";
import { JsonLd } from "@/components/seo/json-ld";
import { ScoringMethodologyPageClient } from "@/features/scoring";
Expand Down Expand Up @@ -68,7 +69,9 @@ export default function ScoringMethodologyPage() {
return (
<>
<JsonLd data={faqSchema} />
<ScoringMethodologyPageClient />
<Suspense fallback={<div className="min-h-screen" />}>
<ScoringMethodologyPageClient />
</Suspense>
</>
);
}
3 changes: 3 additions & 0 deletions src/app/user/[username]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ type CountryInfo = {

const countries = countriesData as CountryInfo[];

export const revalidate = 1800; // Cache user profile on Edge CDN for 30 minutes via ISR
export const dynamicParams = true;

type Props = {
params: Promise<{ username: string }>;
searchParams?: Promise<{ country?: string }>;
Expand Down
60 changes: 44 additions & 16 deletions src/features/leaderboard/services/calculate-leaderboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,29 @@ function getRefreshLimit(): number {
return getEnvInt("LEADERBOARD_REFRESH_LIMIT", 500);
}

function getConcurrency(): number {
return getEnvInt("LEADERBOARD_CONCURRENCY", 3);
}

async function runConcurrent<T>(
items: T[],
concurrency: number,
task: (item: T) => Promise<void>,
): Promise<void> {
if (items.length === 0) return;
const limit = Math.max(1, Math.min(concurrency, items.length));
let currentIndex = 0;

const workers = Array.from({ length: limit }, async () => {
while (currentIndex < items.length) {
const index = currentIndex++;
await task(items[index]);
}
});

await Promise.all(workers);
}

function getSourceUrl(country: string): string {
const template = process.env.LEADERBOARD_SOURCE_URL_TEMPLATE?.trim();
if (!template) {
Expand Down Expand Up @@ -92,6 +115,7 @@ export async function seedNewUsers(
users: LeaderboardSourceEntry[],
seedLimit: number,
staleDays: number,
concurrency: number = getConcurrency(),
): Promise<{
newUsersCount: number;
skippedExistingCount: number;
Expand All @@ -104,15 +128,19 @@ export async function seedNewUsers(
const fetchMetrics: { duration: number; errors: { part: string; reason: string }[] }[] = [];

const usersToSeed = users.slice(0, seedLimit);
const existingSet = await db.getExistingUsernames(usersToSeed.map((u) => u.login));

const usersToFetch: LeaderboardSourceEntry[] = [];
for (const user of usersToSeed) {
try {
const exists = await db.userExists(user.login);
if (exists) {
skippedExistingCount += 1;
continue;
}
if (existingSet.has(user.login.toLowerCase())) {
skippedExistingCount += 1;
} else {
usersToFetch.push(user);
}
}

await runConcurrent(usersToFetch, concurrency, async (user) => {
try {
const { data, metrics } = await getUserData(user.login, {
cacheInRedis: false,
withMetrics: true,
Expand All @@ -130,7 +158,7 @@ export async function seedNewUsers(
} catch (e) {
errors.push({ username: user.login, reason: e instanceof Error ? e.message : String(e) });
}
}
});

return { newUsersCount, skippedExistingCount, errors, fetchMetrics };
}
Expand All @@ -140,6 +168,7 @@ export async function refreshStaleUsers(
country: string,
refreshLimit: number,
staleDays: number,
concurrency: number = getConcurrency(),
): Promise<{
refreshedCount: number;
errors: { username: string; reason: string }[];
Expand All @@ -150,12 +179,10 @@ export async function refreshStaleUsers(
const fetchMetrics: { duration: number; errors: { part: string; reason: string }[] }[] = [];

const topUsers = await db.getTopUsers(country, refreshLimit);
const now = new Date();
const staleUsers = topUsers.filter((row) => row.stale_after < now);

for (const row of topUsers) {
if (row.stale_after >= new Date()) {
continue;
}

await runConcurrent(staleUsers, concurrency, async (row) => {
try {
const { data, metrics } = await getUserData(row.username, {
cacheInRedis: false,
Expand All @@ -174,7 +201,7 @@ export async function refreshStaleUsers(
} catch (e) {
errors.push({ username: row.username, reason: e instanceof Error ? e.message : String(e) });
}
}
});

return { refreshedCount, errors, fetchMetrics };
}
Expand Down Expand Up @@ -242,23 +269,24 @@ export async function calculateLeaderboard(
refreshLimit?: number;
staleDays?: number;
displayLimit?: number;
concurrency?: number;
},
): Promise<CalculateLeaderboardResponse> {
const db = getDatabaseStore();
await db.initializeSchema();

const staleDays = overrides?.staleDays ?? getStaleDays();
const seedLimit = overrides?.seedLimit ?? getSeedLimit();
const refreshLimit = overrides?.refreshLimit ?? getRefreshLimit();
const concurrency = overrides?.concurrency ?? getConcurrency();

// 1. Fetch source users
const sourceData = await fetchCommittersFromTop(country);

// 2a. Seed new users
const seedResult = await seedNewUsers(db, sourceData.users, seedLimit, staleDays);
const seedResult = await seedNewUsers(db, sourceData.users, seedLimit, staleDays, concurrency);

// 2b. Refresh stale users from DB top N
const refreshResult = await refreshStaleUsers(db, country, refreshLimit, staleDays);
const refreshResult = await refreshStaleUsers(db, country, refreshLimit, staleDays, concurrency);

// 3. Build leaderboard result
const allErrors = [
Expand Down
Loading
Loading