Skip to content

Commit 2dfe137

Browse files
committed
feat: limit calendar fetch concurrency to 5 in getOrgDashboardData to prevent 429 rate limits
1 parent a4dcf09 commit 2dfe137

2 files changed

Lines changed: 82 additions & 10 deletions

File tree

lib/github.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
getOrgDashboardData,
1919
getWrappedData,
2020
computeDeveloperScore,
21+
runCappedConcurrency,
2122
} from './github';
2223
import type { ContributionCalendar } from '../types';
2324

@@ -1678,3 +1679,44 @@ describe('computeDeveloperScore', () => {
16781679
).toBe(2);
16791680
});
16801681
});
1682+
1683+
describe('runCappedConcurrency', () => {
1684+
it('should process all items and return their results in order', async () => {
1685+
const items = [1, 2, 3, 4, 5];
1686+
const results = await runCappedConcurrency(items, 2, async (x) => {
1687+
return x * 10;
1688+
});
1689+
1690+
expect(results).toEqual([10, 20, 30, 40, 50]);
1691+
});
1692+
1693+
it('should strictly limit the concurrency to the specified limit', async () => {
1694+
const items = [1, 2, 3, 4, 5];
1695+
let activePromises = 0;
1696+
let maxConcurrentPromises = 0;
1697+
1698+
await runCappedConcurrency(items, 2, async (x) => {
1699+
activePromises++;
1700+
maxConcurrentPromises = Math.max(maxConcurrentPromises, activePromises);
1701+
await new Promise((resolve) => setTimeout(resolve, 5));
1702+
activePromises--;
1703+
return x;
1704+
});
1705+
1706+
expect(maxConcurrentPromises).toBeLessThanOrEqual(2);
1707+
});
1708+
1709+
it('should handle errors inside tasks gracefully without aborting the entire sequence', async () => {
1710+
const items = [1, 2, 3, 4, 5];
1711+
const results = await runCappedConcurrency(items, 2, async (x) => {
1712+
if (x === 3) throw new Error('Task 3 failed');
1713+
return x * 10;
1714+
});
1715+
1716+
expect(results[0]).toBe(10);
1717+
expect(results[1]).toBe(20);
1718+
expect(results[2]).toBeNull();
1719+
expect(results[3]).toBe(40);
1720+
expect(results[4]).toBe(50);
1721+
});
1722+
});

lib/github.ts

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -705,16 +705,14 @@ export async function getOrgDashboardData(orgName: string, options: FetchOptions
705705

706706
const members = membersOrError;
707707

708-
// Fetch calendars for all members concurrently (Capped by member limit to avoid 429)
709-
const memberCalendarsPromises = members.map((member: string) =>
710-
fetchGitHubContributions(member, options)
711-
.then((data) => data.calendar)
712-
.catch(() => null)
713-
);
714-
715-
const calendars = (await Promise.all(memberCalendarsPromises)).filter(
716-
(c: ContributionCalendar | null) => c !== null
717-
) as ContributionCalendar[];
708+
// Fetch calendars for all members concurrently with capped concurrency to avoid 429s/timeouts
709+
const calendars = (
710+
await runCappedConcurrency(members, 5, (member) =>
711+
fetchGitHubContributions(member, options)
712+
.then((data) => data.calendar)
713+
.catch(() => null)
714+
)
715+
).filter((c: ContributionCalendar | null) => c !== null) as ContributionCalendar[];
718716

719717
// Create the Mega-City
720718
const aggregatedCalendar = aggregateCalendars(calendars);
@@ -1227,3 +1225,35 @@ export async function getWrappedData(
12271225
topLanguage,
12281226
};
12291227
}
1228+
1229+
/**
1230+
* Run tasks concurrently with a maximum limit on active promises.
1231+
*/
1232+
export async function runCappedConcurrency<T, R>(
1233+
items: T[],
1234+
limit: number,
1235+
fn: (item: T) => Promise<R>
1236+
): Promise<R[]> {
1237+
const results: R[] = new Array(items.length);
1238+
let currentIndex = 0;
1239+
1240+
async function worker(): Promise<void> {
1241+
while (currentIndex < items.length) {
1242+
const index = currentIndex++;
1243+
try {
1244+
results[index] = await fn(items[index]);
1245+
} catch (err) {
1246+
results[index] = null as unknown as R;
1247+
}
1248+
}
1249+
}
1250+
1251+
const workers: Promise<void>[] = [];
1252+
const workerCount = Math.min(limit, items.length);
1253+
for (let i = 0; i < workerCount; i++) {
1254+
workers.push(worker());
1255+
}
1256+
1257+
await Promise.all(workers);
1258+
return results;
1259+
}

0 commit comments

Comments
 (0)