Spaces:
Build error
Build error
| import express from 'express'; | |
| import path from 'path'; | |
| import { fileURLToPath } from 'url'; | |
| const __dirname = path.dirname(fileURLToPath(import.meta.url)); | |
| const app = express(); | |
| const PORT = process.env.PORT || 7860; | |
| // Cache configuration | |
| const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours | |
| const OFFICIAL_APP_LIST_URL = 'https://huggingface.co/datasets/pollen-robotics/reachy-mini-official-app-store/raw/main/app-list.json'; | |
| const HF_SPACES_API = 'https://huggingface.co/api/spaces'; | |
| // Note: HF API doesn't support pagination with filter=, so we use a high limit | |
| const HF_SPACES_LIMIT = 1000; | |
| // In-memory cache | |
| let appsCache = { | |
| data: null, | |
| lastFetch: null, | |
| fetching: false, | |
| }; | |
| // Fetch apps from HuggingFace API | |
| async function fetchAppsFromHF() { | |
| console.log('[Cache] Fetching apps from HuggingFace API...'); | |
| try { | |
| // 1. Fetch official app IDs | |
| const officialResponse = await fetch(OFFICIAL_APP_LIST_URL); | |
| let officialIdList = []; | |
| if (officialResponse.ok) { | |
| officialIdList = await officialResponse.json(); | |
| } | |
| const officialSet = new Set(officialIdList.map(id => id.toLowerCase())); | |
| // 2. Fetch all spaces with reachy_mini tag | |
| // Note: HF API doesn't support pagination with filter=, so we use a high limit | |
| const spacesResponse = await fetch(`${HF_SPACES_API}?filter=reachy_mini&full=true&limit=${HF_SPACES_LIMIT}`); | |
| if (!spacesResponse.ok) { | |
| throw new Error(`HF API returned ${spacesResponse.status}`); | |
| } | |
| const allSpaces = await spacesResponse.json(); | |
| console.log(`[Cache] Fetched ${allSpaces.length} spaces from HuggingFace`); | |
| // 3. Build apps list with isOfficial and isPythonApp flags | |
| const allApps = allSpaces.map(space => { | |
| const spaceId = space.id || ''; | |
| const tags = space.tags || []; | |
| const isOfficial = officialSet.has(spaceId.toLowerCase()); | |
| const isPythonApp = tags.includes('reachy_mini_python_app'); | |
| return { | |
| id: spaceId, | |
| name: spaceId.split('/').pop(), | |
| description: space.cardData?.short_description || '', | |
| cardData: space.cardData || {}, | |
| likes: space.likes || 0, | |
| lastModified: space.lastModified, | |
| author: spaceId.split('/')[0], | |
| isOfficial, | |
| isPythonApp, | |
| tags, | |
| }; | |
| }); | |
| // Sort: official first, then by likes | |
| allApps.sort((a, b) => { | |
| if (a.isOfficial !== b.isOfficial) { | |
| return a.isOfficial ? -1 : 1; | |
| } | |
| return (b.likes || 0) - (a.likes || 0); | |
| }); | |
| return allApps; | |
| } catch (err) { | |
| console.error('[Cache] Error fetching apps:', err); | |
| throw err; | |
| } | |
| } | |
| // Get apps with caching | |
| async function getApps() { | |
| const now = Date.now(); | |
| // Return cache if valid | |
| if (appsCache.data && appsCache.lastFetch && (now - appsCache.lastFetch) < CACHE_TTL_MS) { | |
| const ageMinutes = Math.round((now - appsCache.lastFetch) / 60000); | |
| console.log(`[Cache] Returning cached data (age: ${ageMinutes} min)`); | |
| return appsCache.data; | |
| } | |
| // Prevent concurrent fetches | |
| if (appsCache.fetching) { | |
| console.log('[Cache] Fetch already in progress, returning stale data'); | |
| return appsCache.data || []; | |
| } | |
| appsCache.fetching = true; | |
| try { | |
| const apps = await fetchAppsFromHF(); | |
| appsCache.data = apps; | |
| appsCache.lastFetch = now; | |
| console.log(`[Cache] Cache updated with ${apps.length} apps`); | |
| return apps; | |
| } catch (err) { | |
| // On error, return stale cache if available | |
| if (appsCache.data) { | |
| console.log('[Cache] Fetch failed, returning stale cache'); | |
| return appsCache.data; | |
| } | |
| throw err; | |
| } finally { | |
| appsCache.fetching = false; | |
| } | |
| } | |
| // API endpoint | |
| app.get('/api/apps', async (req, res) => { | |
| try { | |
| const apps = await getApps(); | |
| res.json({ | |
| apps, | |
| cached: true, | |
| cacheAge: appsCache.lastFetch ? Math.round((Date.now() - appsCache.lastFetch) / 1000) : 0, | |
| count: apps.length, | |
| }); | |
| } catch (err) { | |
| console.error('[API] Error:', err); | |
| res.status(500).json({ error: 'Failed to fetch apps' }); | |
| } | |
| }); | |
| // Health check | |
| app.get('/api/health', (req, res) => { | |
| res.json({ | |
| status: 'ok', | |
| cacheStatus: appsCache.data ? 'warm' : 'cold', | |
| cacheAge: appsCache.lastFetch ? Math.round((Date.now() - appsCache.lastFetch) / 1000) : null, | |
| appsCount: appsCache.data?.length || 0, | |
| }); | |
| }); | |
| // Force cache refresh (for admin use) | |
| app.post('/api/refresh', async (req, res) => { | |
| try { | |
| appsCache.lastFetch = null; // Invalidate cache | |
| const apps = await getApps(); | |
| res.json({ success: true, count: apps.length }); | |
| } catch (err) { | |
| res.status(500).json({ error: 'Failed to refresh cache' }); | |
| } | |
| }); | |
| // Serve static files from the dist folder | |
| app.use(express.static(path.join(__dirname, '../dist'), { | |
| maxAge: '1y', | |
| etag: true, | |
| })); | |
| // SPA fallback - serve index.html for all other routes | |
| app.get('*', (req, res) => { | |
| res.sendFile(path.join(__dirname, '../dist/index.html')); | |
| }); | |
| // Pre-warm cache on startup | |
| async function warmCache() { | |
| console.log('[Startup] Pre-warming cache...'); | |
| try { | |
| await getApps(); | |
| console.log('[Startup] Cache warmed successfully'); | |
| } catch (err) { | |
| console.error('[Startup] Failed to warm cache:', err); | |
| } | |
| } | |
| // Start server | |
| app.listen(PORT, () => { | |
| console.log(`[Server] Running on port ${PORT}`); | |
| warmCache(); | |
| }); | |