import { CONFIG } from "./config.js"; import { normalizeTrack } from "./tracks.js"; const DEFAULT_EMOJI = "🛖"; export function parseLinkHeader(header) { if (!header) return null; for (const part of header.split(",")) { const m = part.match(/<([^>]+)>\s*;\s*rel="?next"?/); if (m) return m[1]; } return null; } export function filterEligible(spaces, denylist = CONFIG.denylist) { const deny = new Set(denylist); return spaces.filter((s) => !deny.has(s.id) && s.private !== true); } export function toViewModel(s) { const card = s.cardData || {}; const name = s.id.includes("/") ? s.id.split("/").slice(1).join("/") : s.id; const subdomain = s.subdomain || s.id.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, ""); // Static Spaces are served from .static.hf.space; everything else from .hf.space. const host = s.sdk === "static" ? `${subdomain}.static.hf.space` : `${subdomain}.hf.space`; return { id: s.id, name, title: card.title || name, emoji: card.emoji || DEFAULT_EMOJI, shortDescription: card.short_description || "", likes: typeof s.likes === "number" ? s.likes : 0, sdk: s.sdk || "", track: normalizeTrack(card.tags), colorFrom: card.colorFrom || "gray", colorTo: card.colorTo || "gray", embedUrl: `https://${host}`, hfUrl: `${CONFIG.apiBase}/spaces/${s.id}`, }; } // ---- network (live-verified) ---- export async function fetchAllSpaces() { let url = `${CONFIG.apiBase}/api/spaces?author=${CONFIG.org}&full=true&limit=${CONFIG.apiLimit}`; const all = []; for (let guard = 0; url && guard < 20; guard++) { const res = await fetch(url); if (!res.ok) throw new Error(`list fetch ${res.status}`); const page = await res.json(); all.push(...page); url = parseLinkHeader(res.headers.get("Link")); } return filterEligible(all).map(toViewModel); } export async function fetchAllSpacesWithRetry() { try { return await fetchAllSpaces(); } catch { return await fetchAllSpaces(); } // retry once; second failure throws to caller } export async function fetchDetail(id) { const res = await fetch(`${CONFIG.apiBase}/api/spaces/${id}`); if (!res.ok) throw new Error(`detail ${res.status}`); const d = await res.json(); const stage = d?.runtime?.stage || null; const domainsReady = Array.isArray(d?.runtime?.domains) ? d.runtime.domains.some((x) => x.stage === "READY") : false; return { stage, domainsReady, gated: !!d.gated, disabled: !!d.disabled, likes: d.likes }; }