pr-monitor/server/github/client.ts
2026-07-28 14:21:25 +00:00

59 lines
1.6 KiB
TypeScript

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<T>(
query: string,
variables: Record<string, unknown>,
): Promise<T> {
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;
}
}