MEDICAL / index.js
VIATEUR-AI's picture
Create index.js
ff394a4 verified
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();
});