/** * Backend API helper. * * Centralises the BACKEND_URL constant and provides a thin fetch wrapper that: * - Resolves paths relative to BACKEND_API_URL. * - Throws 502 on network errors (unreachable backend). * - Re-throws SvelteKit `error()` objects so callers can still short-circuit. * - Passes a RequestInit through verbatim so callers keep full control. * * Import only from server-side modules (`+server.ts`, `*.server.ts`). */ import { error } from '@sveltejs/kit'; import { env } from '$env/dynamic/private'; import * as cache from '$lib/server/cache'; export const BACKEND_URL = env.BACKEND_API_URL ?? 'http://localhost:8080'; /** * Fetch a path on the backend, throwing a 502 on network failures. * * The `path` must start with `/` (e.g. `/api/voices`). * * SvelteKit `error()` exceptions are always re-thrown so callers can * short-circuit correctly inside their own catch blocks. */ export async function backendFetch(path: string, init?: RequestInit): Promise { try { return await fetch(`${BACKEND_URL}${path}`, init); } catch (e) { // Re-throw SvelteKit HTTP errors so they propagate to the framework. if (e instanceof Error && 'status' in e) throw e; throw error(502, 'Could not reach backend'); } } // ─── Cached admin model lists ───────────────────────────────────────────────── const MODELS_CACHE_TTL = 10 * 60; // 10 minutes — model lists rarely change /** * Fetch image-gen model list from the Go backend with a 10-minute cache. * Returns an empty array on error (callers should surface a warning). */ export async function listImageModels(): Promise { const key = 'backend:models:image-gen'; const cached = await cache.get(key); if (cached) return cached; try { const res = await backendFetch('/api/admin/image-gen/models'); if (!res.ok) return []; const data = await res.json(); const models = (data.models ?? []) as T[]; await cache.set(key, models, MODELS_CACHE_TTL); return models; } catch { return []; } } /** * Fetch text-gen model list from the Go backend with a 10-minute cache. * Returns an empty array on error. */ export async function listTextModels(): Promise { const key = 'backend:models:text-gen'; const cached = await cache.get(key); if (cached) return cached; try { const res = await backendFetch('/api/admin/text-gen/models'); if (!res.ok) return []; const data = await res.json(); const models = (data.models ?? []) as T[]; await cache.set(key, models, MODELS_CACHE_TTL); return models; } catch { return []; } } // ─── Response types ─────────────────────────────────────────────────────────── /** * Metadata shape returned inside the 200 response from GET /api/book-preview/{slug}. * Used in both the SSR page load and the API proxy to avoid duplicating the inline type. */ export interface BookPreviewMeta { slug: string; title: string; author: string; cover: string; status: string; genres: string[]; summary: string; total_chapters: number; source_url: string; } /** Full 200 response from GET /api/book-preview/{slug}. */ export interface BookPreviewResponse { in_lib: boolean; meta: BookPreviewMeta; chapters: { number: number; title: string; date?: string }[]; }