(args, parseFn)
| 409 | } |
| 410 | |
| 411 | async #executeWithBackoff(args, parseFn) { |
| 412 | // "Basics": only retry once, and only if we can infer a short wait. |
| 413 | for (let attempt = 0; attempt < 2; attempt++) { |
| 414 | const res = await this.ghRunner(args); |
| 415 | if (res.timedOut) throw new Error(`gh timed out: gh ${args.join(' ')}`); |
| 416 | if (res.exitCode !== 0 && !res.stdout) { |
| 417 | throw new Error(`gh failed (exit ${res.exitCode}): ${res.stderr || 'unknown error'}`); |
| 418 | } |
| 419 | |
| 420 | const parsed = parseFn(res.stdout); |
| 421 | const status = parsed.status; |
| 422 | const headers = parsed.headers || {}; |
| 423 | |
| 424 | const isRateLimited = |
| 425 | status === 429 || |
| 426 | (status === 403 && String(headers['x-ratelimit-remaining'] || '') === '0'); |
| 427 | |
| 428 | if (!isRateLimited || attempt === 1) return parsed; |
| 429 | |
| 430 | const retryAfter = Number(headers['retry-after'] || 'NaN'); |
| 431 | const resetEpoch = Number(headers['x-ratelimit-reset'] || 'NaN'); |
| 432 | const nowEpoch = Math.floor(Date.now() / 1000); |
| 433 | |
| 434 | let waitSeconds = Number.isFinite(retryAfter) ? retryAfter : NaN; |
| 435 | if (!Number.isFinite(waitSeconds) && Number.isFinite(resetEpoch)) { |
| 436 | waitSeconds = Math.max(0, resetEpoch - nowEpoch); |
| 437 | } |
| 438 | if (!Number.isFinite(waitSeconds)) { |
| 439 | // Can't infer; don't spin. |
| 440 | return parsed; |
| 441 | } |
| 442 | |
| 443 | waitSeconds = Math.min(waitSeconds, this.maxBackoffSeconds); |
| 444 | if (waitSeconds > 0) { |
| 445 | await this.sleepFn(waitSeconds * 1000); |
| 446 | } |
| 447 | } |
| 448 | |
| 449 | // Unreachable |
| 450 | throw new Error('unexpected backoff loop state'); |
| 451 | } |
| 452 | } |
| 453 |
no outgoing calls
no test coverage detected