import express from 'express'; import { createServer } from 'http'; import { Server } from 'socket.io'; import path from 'path'; import { fileURLToPath } from 'url'; import fs from 'fs/promises'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); export function startServer(categorySyncer, categoryProductsSyncer) { const app = express(); const httpServer = createServer(app); const io = new Server(httpServer); const PORT = process.env.SERVER_PORT || 3000; const HOST = process.env.SERVER_HOST || '0.0.0.0'; const CACHE_DIR = process.env.CACHE_LOCATION || './cache'; // Cache for ETags and data const cache = { categories: { etag: null, data: null }, html: { etag: null, data: null }, products: new Map() // id -> { etag, data } }; // Function to calculate ETag for categories async function updateCategoriesCache() { try { const treePath = path.join(CACHE_DIR, 'category_tree.json'); const data = await fs.readFile(treePath, 'utf-8'); const crypto = await import('crypto'); cache.categories.etag = crypto.createHash('md5').update(data).digest('hex'); cache.categories.data = data; } catch (err) { // Silently skip if file doesn't exist yet (will be created on first sync) if (err.code !== 'ENOENT') { console.error('Error updating categories cache:', err); } } } // Function to calculate ETag for a specific category's products async function updateProductCache(id) { try { const productPath = path.join(CACHE_DIR, 'products', `category_${id}.json`); const data = await fs.readFile(productPath, 'utf-8'); const crypto = await import('crypto'); const etag = crypto.createHash('md5').update(data).digest('hex'); cache.products.set(id, { etag, data }); } catch (err) { // If file missing, remove from cache if (err.code === 'ENOENT') { cache.products.delete(id); } else { console.error(`Error updating product cache for category ${id}:`, err); } } } // Function to calculate ETag for HTML async function updateHtmlCache() { try { const htmlPath = path.join(__dirname, 'index.html'); const data = await fs.readFile(htmlPath, 'utf-8'); const crypto = await import('crypto'); cache.html.etag = crypto.createHash('md5').update(data).digest('hex'); cache.html.data = data; } catch (err) { console.error('Error updating HTML cache:', err); } } // Initialize caches on startup updateHtmlCache(); updateCategoriesCache(); // Update categories cache when sync completes if (categorySyncer) { categorySyncer.on('synced', ({ changed }) => { if (changed) { updateCategoriesCache(); io.emit('categoriesUpdated'); } }); } // Update product cache when category products update if (categoryProductsSyncer) { categoryProductsSyncer.on('categoryUpdated', ({ id }) => { updateProductCache(id); io.emit('categoryProductsUpdated', { id }); }); } // Socket.io connection io.on('connection', (socket) => { console.log('🔌 Client connected'); socket.on('disconnect', () => { console.log('🔌 Client disconnected'); }); }); // Serve category tree JSON (with ETag for conditional caching) app.get('/api/categories', async (req, res) => { try { // Check if client has cached version if (req.headers['if-none-match'] === cache.categories.etag) { return res.status(304).end(); // Not Modified } // Set cache headers with ETag res.set('Cache-Control', 'public, max-age=60, must-revalidate'); res.set('ETag', cache.categories.etag); res.json(JSON.parse(cache.categories.data)); } catch (err) { res.status(500).json({ error: 'Failed to load category tree' }); } }); // Serve category products JSON (with ETag) app.get('/api/categories/:id/products', async (req, res) => { try { const id = parseInt(req.params.id); // Lazy load if not in cache if (!cache.products.has(id)) { await updateProductCache(id); } const cached = cache.products.get(id); if (!cached) { return res.status(404).json({ error: 'Category products not found' }); } // Check if client has cached version if (req.headers['if-none-match'] === cached.etag) { return res.status(304).end(); // Not Modified } // Set cache headers with ETag res.set('Cache-Control', 'public, max-age=60, must-revalidate'); res.set('ETag', cached.etag); res.json(JSON.parse(cached.data)); } catch (err) { console.error(`Error serving products for category ${req.params.id}:`, err); res.status(500).json({ error: 'Failed to load products' }); } }); // Serve category images (long cache - images rarely change) app.get('/img/cat/:id.avif', (req, res) => { const { id } = req.params; const imagePath = path.join(CACHE_DIR, 'img', 'categories', `${id}.avif`); // Cache images for 1 year (immutable content) res.set('Cache-Control', 'public, max-age=31536000, immutable'); res.sendFile(path.resolve(imagePath), (err) => { if (err) { res.status(404).send('Image not found'); } }); }); // Serve index.html (with ETag for conditional caching) app.get('/', async (req, res) => { try { // Check if client has cached version if (req.headers['if-none-match'] === cache.html.etag) { return res.status(304).end(); // Not Modified } // Set cache headers with ETag res.set('Cache-Control', 'public, max-age=300, must-revalidate'); res.set('ETag', cache.html.etag); res.set('Content-Type', 'text/html'); res.send(cache.html.data); } catch (err) { console.error('Error serving index.html:', err); res.status(500).send('Error loading page'); } }); httpServer.listen(PORT, HOST, () => { console.log(`🌐 Server running on http://${HOST}:${PORT}`); }); }