|
1 | 1 | import fs from "node:fs/promises";
|
| 2 | +import timers from "node:timers/promises"; |
2 | 3 | import { URL } from "node:url";
|
3 | 4 |
|
4 | 5 | const MAINTAINERS = {
|
@@ -221,16 +222,61 @@ class UserFetchError extends Error {
|
221 | 222 | }
|
222 | 223 | }
|
223 | 224 |
|
224 |
| -async function fetchUserInfo(username) { |
225 |
| - const res = await fetch(`https://api.github.com/users/${username}`, { |
| 225 | +const BACKOFF_INTERVALS_MINUTES = [1, 2, 3, 5]; |
| 226 | +const MAX_RETRIES = BACKOFF_INTERVALS_MINUTES.length - 1; |
| 227 | + |
| 228 | +async function rateLimitDelay({ retryAfter, ratelimitReset, retryCount }) { |
| 229 | + let timeoutInMilliseconds = BACKOFF_INTERVALS_MINUTES[retryCount] * 1000; |
| 230 | + if (retryAfter) { |
| 231 | + timeoutInMilliseconds = retryAfter * 1000; |
| 232 | + } else if (ratelimitRemaining === "0" && ratelimitReset) { |
| 233 | + timeoutInMilliseconds = ratelimitReset * 1000 - Date.now(); |
| 234 | + } |
| 235 | + |
| 236 | + const timeoutInSeconds = (timeoutInMilliseconds / 1000).toLocaleString(); |
| 237 | + console.warn(`Waiting for ${timeoutInSeconds} seconds...`); |
| 238 | + |
| 239 | + await timers.setTimeout(timeoutInMilliseconds); |
| 240 | +} |
| 241 | + |
| 242 | +async function fetchUserInfo(username, retryCount = 0) { |
| 243 | + if (retryCount >= MAX_RETRIES) { |
| 244 | + throw new Error(`Hit max retries (${MAX_RETRIES}) for fetching user ${username}`); |
| 245 | + } |
| 246 | + |
| 247 | + const request = new Request(`https://api.github.com/users/${username}`, { |
226 | 248 | headers: {
|
227 | 249 | Accept: "application/vnd.github+json",
|
228 | 250 | "X-GitHub-Api-Version": "2022-11-28",
|
229 | 251 | },
|
230 | 252 | });
|
| 253 | + if (process.env.GITHUB_TOKEN) { |
| 254 | + request.headers.set("Authorization", `Bearer ${process.env.GITHUB_TOKEN}`); |
| 255 | + } |
| 256 | + |
| 257 | + const res = await fetch(request); |
| 258 | + |
231 | 259 | if (!res.ok) {
|
| 260 | + const retryAfter = res.headers.get("retry-after"); // seconds |
| 261 | + const ratelimitRemaining = res.headers.get("x-ratelimit-remaining"); // quantity of requests remaining |
| 262 | + const ratelimitReset = res.headers.get("x-ratelimit-reset"); // UTC epoch seconds |
| 263 | + |
| 264 | + if (retryAfter || ratelimitRemaining === "0" || ratelimitReset) { |
| 265 | + // See https://docs.github.com/en/rest/using-the-rest-api/best-practices-for-using-the-rest-api?apiVersion=2022-11-28#handle-rate-limit-errors-appropriately |
| 266 | + console.warn("Rate limited by GitHub API"); |
| 267 | + |
| 268 | + await rateLimitDelay({ |
| 269 | + retryAfter, |
| 270 | + ratelimitReset, |
| 271 | + retryCount, |
| 272 | + }); |
| 273 | + |
| 274 | + return await fetchUserInfo(username, retryCount + 1); |
| 275 | + } |
| 276 | + |
232 | 277 | throw new UserFetchError(`${res.url} responded with ${res.status}`, res);
|
233 | 278 | }
|
| 279 | + |
234 | 280 | return await res.json();
|
235 | 281 | }
|
236 | 282 |
|
@@ -283,4 +329,6 @@ async function main() {
|
283 | 329 | }
|
284 | 330 | }
|
285 | 331 |
|
286 |
| -main(); |
| 332 | +if (import.meta.main) { |
| 333 | + main(); |
| 334 | +} |
0 commit comments