Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DgBEW6qAwgn2fcQbA2f4ZQ
59 lines
1.6 KiB
TypeScript
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;
|
|
}
|
|
}
|