evOo-local/apps/www/src/github.ts
cynarAI 6c7eed7580 Initial clean evOo-local from f00577b
Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline
(Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/*
packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
2026-09-20 17:15:16 +02:00

70 lines
2.2 KiB
TypeScript

import { readBoundedJsonResponse } from "@rakazo/core";
import { GITHUB_API_REPO } from "./site";
export const GITHUB_STARS_TIMEOUT_MS = 5_000;
export const MAX_GITHUB_STARS_RESPONSE_BYTES = 64 * 1024;
export async function fetchGithubStars(fetchImpl: typeof fetch = fetch): Promise<number | null> {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), GITHUB_STARS_TIMEOUT_MS);
try {
const response = await withAbort(
fetchImpl(GITHUB_API_REPO, {
headers: {
Accept: "application/vnd.github+json",
"User-Agent": "evoo-www",
},
signal: controller.signal,
}),
controller.signal,
);
if (!response.ok) {
cancelResponseBody(response);
return null;
}
const payload = await readBoundedJsonResponse<{
stargazers_count?: unknown;
}>(response, MAX_GITHUB_STARS_RESPONSE_BYTES, controller.signal);
return typeof payload.stargazers_count === "number" ? payload.stargazers_count : null;
} catch {
return null;
} finally {
clearTimeout(timer);
}
}
function cancelResponseBody(response: Response): void {
try {
void Promise.resolve(response.body?.cancel()).catch(() => undefined);
} catch {
// Best-effort release only; cancellation must not delay static rendering.
}
}
function withAbort<T>(promise: Promise<T>, signal: AbortSignal): Promise<T> {
if (signal.aborted) return Promise.reject(signal.reason ?? new Error("Request timed out"));
return new Promise<T>((resolve, reject) => {
const onAbort = () => reject(signal.reason ?? new Error("Request timed out"));
signal.addEventListener("abort", onAbort, { once: true });
promise.then(
(value) => {
signal.removeEventListener("abort", onAbort);
resolve(value);
},
(error: unknown) => {
signal.removeEventListener("abort", onAbort);
reject(error);
},
);
});
}
export function formatStarCount(count: number): string {
if (count < 1000) {
return String(count);
}
const thousands = count / 1000;
const rounded = thousands >= 10 ? Math.round(thousands) : Math.round(thousands * 10) / 10;
return `${rounded}k`.replace(/\.0k$/, "k");
}