import { logDebug, logError, logWarn } from "./logger.js"; export async function api(path, options = {}) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 12000); try { const headers = new Headers(options.headers || {}); if (!(options.body instanceof FormData) && !headers.has("content-type")) headers.set("content-type", "application/json"); const started = performance.now(); logDebug("api.request", { method: options.method || "GET", path }); const response = await fetch(path, { ...options, headers, signal: controller.signal }); const durationMs = Math.round(performance.now() - started); logDebug("api.response", { method: options.method || "GET", path, status: response.status, durationMs }); const contentType = response.headers.get("content-type") || ""; const data = contentType.includes("application/json") ? await response.json().catch(() => ({})) : {}; if (!response.ok) { const defaults = { 400: "Invalid request.", 401: "Authentication required.", 403: "Access denied.", 404: "The requested resource was not found.", 405: "This operation is not allowed.", 409: "The requested change conflicts with existing data.", 413: "The uploaded data is too large.", 429: "Too many requests. Try again later.", 500: "Server error. Try again later.", 503: "Service temporarily unavailable." }; const requestError = new Error(data.error || defaults[response.status] || `Request failed (${response.status}).`); requestError.status = response.status; logWarn("api.failed", { method: options.method || "GET", path, status: response.status, message: requestError.message }); throw requestError; } return data; } catch (error) { if (error.name === "AbortError") { logWarn("api.timeout", { method: options.method || "GET", path }); throw new Error("Timed out"); } logError("api.network_error", error, { method: options.method || "GET", path }); throw error; } finally { clearTimeout(timeout); } }