Spaces:
Running
Running
| 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 <subdomain>.static.hf.space; everything else from <subdomain>.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 }; | |
| } | |