import { z } from "zod"; // Import client-db index directly as requested. // On the server, `db` will be null (because `browser` is false in that module). import { db, ensureSeeded, getSongsForMalAnimeIds } from "$lib/db/client-db"; import { MalAnimeListQuerySchema, MalAnimeListResponseSchema, MalAnimeListStatusEnum, } from "$lib/types/mal"; import { songTypesCodec } from "../songs/schema"; import type { PageLoad } from "./$types"; const LIST_QUERY_LIMIT = 1000; const SearchSchema = MalAnimeListQuerySchema.extend({ // Username mal: z.string().optional(), // Allow empty string to mean "All" status: MalAnimeListStatusEnum.or(z.literal("")).optional(), type: songTypesCodec.optional(), }).strict(); type StatusParam = z.infer["status"]; function normalizeStatus( status: StatusParam, ): z.infer | undefined { if (status == null || status === "") return undefined; return status; } export const load: PageLoad = async ({ url, fetch, depends }) => { depends("mal:animelist"); depends("clientdb:songs"); const parsed = SearchSchema.safeParse( Object.fromEntries(url.searchParams.entries()), ); const mal = parsed.success ? parsed.data.mal : undefined; const status = parsed.success ? normalizeStatus(parsed.data.status) : undefined; const types = parsed.success ? parsed.data.type : undefined; const username = (mal ?? "").trim(); // Always return a stable shape for hydration if (!username) { return { LIST_QUERY_LIMIT, username: "", status: status ?? null, malResponse: null as z.infer | null, songRows: [] as Awaited>, }; } // This endpoint proxies MAL and works server-side. const malUrl = new URL( `/api/mal/animelist/${encodeURIComponent(username)}`, url.origin, ); malUrl.searchParams.set("limit", String(LIST_QUERY_LIMIT)); if (status) malUrl.searchParams.set("status", status); // NOTE: If you later want to support sort/offset, add them here from SearchSchema too. const malRes = await fetch(malUrl); if (!malRes.ok) { // Let +page.svelte decide how to display errors; throw to use SvelteKit error page throw new Error(`MAL request failed (${malRes.status})`); } const malJson: unknown = await malRes.json(); const malResponse = MalAnimeListResponseSchema.parse(malJson); // Client-only DB: on the server `db` is null, so return [] and let hydration re-run load in browser. if (!db) { return { LIST_QUERY_LIMIT, username, status: status ?? null, malResponse, songRows: [] as Awaited>, }; } // Browser path: seed then query local DB for songs by MAL ids await ensureSeeded({ fetch }); const malIds = malResponse.data.map((e) => e.node.id); const songRows = await getSongsForMalAnimeIds(db, malIds, types); return { LIST_QUERY_LIMIT, username, status: status ?? null, malResponse, songRows, }; };