File size: 2,548 Bytes
653b61d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6663eb3
 
653b61d
 
 
 
 
 
 
 
 
 
 
6663eb3
653b61d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
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 };
}