mission-control/lib/data/gantt-api.ts

107 lines
3.0 KiB
TypeScript

const DEFAULT_GANTT_API_BASE_URL = "http://localhost:3000/api";
const DEFAULT_REVALIDATE_SECONDS = 15;
type CacheEntry = {
expiresAt: number;
value: unknown;
promise?: Promise<unknown>;
};
const responseCache = new Map<string, CacheEntry>();
function normalizeBaseUrl(url: string): string {
return url.replace(/\/+$/, "");
}
export function getGanttApiBaseUrl(): string {
const configured =
process.env.GANTT_API_BASE_URL ||
process.env.NEXT_PUBLIC_GANTT_API_BASE_URL ||
DEFAULT_GANTT_API_BASE_URL;
return normalizeBaseUrl(configured);
}
function getRevalidateSeconds(): number {
const raw = Number(process.env.GANTT_API_REVALIDATE_SECONDS || String(DEFAULT_REVALIDATE_SECONDS));
return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_REVALIDATE_SECONDS;
}
function buildAuthHeaders(): HeadersInit {
const headers: Record<string, string> = {};
if (process.env.GANTT_API_BEARER_TOKEN) {
headers.Authorization = `Bearer ${process.env.GANTT_API_BEARER_TOKEN}`;
}
if (process.env.GANTT_API_COOKIE) {
headers.Cookie = process.env.GANTT_API_COOKIE;
}
return headers;
}
export async function fetchGanttApi<T>(endpoint: string): Promise<T> {
const baseUrl = getGanttApiBaseUrl();
const revalidateSeconds = getRevalidateSeconds();
const cacheKey = `${baseUrl}${endpoint}`;
const now = Date.now();
const cached = responseCache.get(cacheKey);
if (cached && cached.expiresAt > now) {
if (cached.promise) {
return (await cached.promise) as T;
}
return cached.value as T;
}
const requestPromise = (async () => {
const response = await fetch(cacheKey, {
method: "GET",
cache: "no-store",
headers: {
Accept: "application/json",
...buildAuthHeaders(),
},
});
const contentType = response.headers.get("content-type") || "";
const isJson = contentType.includes("application/json");
const payload = (isJson ? await response.json().catch(() => null) : null) as
| { error?: string; message?: string }
| null;
const nonJsonBody = !isJson ? await response.text().catch(() => "") : "";
if (!response.ok) {
const details =
payload?.error ||
payload?.message ||
(nonJsonBody ? nonJsonBody.replace(/\s+/g, " ").slice(0, 200) : response.statusText);
throw new Error(`gantt-board API request failed (${response.status} ${endpoint}): ${details}`);
}
if (!isJson) {
throw new Error(`gantt-board API request failed (${response.status} ${endpoint}): expected JSON response`);
}
return payload as T;
})();
responseCache.set(cacheKey, {
expiresAt: now + revalidateSeconds * 1000,
value: null,
promise: requestPromise as Promise<unknown>,
});
try {
const result = await requestPromise;
responseCache.set(cacheKey, {
expiresAt: Date.now() + revalidateSeconds * 1000,
value: result as unknown,
});
return result;
} catch (error) {
responseCache.delete(cacheKey);
throw error;
}
}