import { getToken, invalidateToken } from './token.js'; export interface RateLimitInfo { cost: number; remaining: number; resetAt: string; } export class GithubError extends Error {} let lastRateLimit: RateLimitInfo | null = null; export function getLastRateLimit(): RateLimitInfo | null { return lastRateLimit; } export async function graphql( query: string, variables: Record, ): Promise { for (let attempt = 0; ; attempt++) { const res = await fetch('https://api.github.com/graphql', { method: 'POST', headers: { authorization: `bearer ${getToken()}`, 'content-type': 'application/json', 'user-agent': 'pr-monitor', }, body: JSON.stringify({ query, variables }), }); if (res.status === 401 && attempt === 0) { invalidateToken(); continue; } if (!res.ok) { throw new GithubError(`GitHub API ${res.status}: ${(await res.text()).slice(0, 300)}`); } const body = (await res.json()) as { data?: T & { rateLimit?: RateLimitInfo }; errors?: { message: string }[]; }; // Partial data with errors (e.g. one inaccessible node) is usable; only fail when // there is no data at all. if (!body.data) { const msg = body.errors?.map((e) => e.message).join('; ') ?? 'no data returned'; throw new GithubError(`GraphQL error: ${msg.slice(0, 500)}`); } if (body.data.rateLimit) { lastRateLimit = body.data.rateLimit; console.log( `[github] cost=${lastRateLimit.cost} remaining=${lastRateLimit.remaining}`, ); } return body.data; } }