This commit is contained in:
sebseb7
2025-11-23 11:45:42 +01:00
parent 71910f84a2
commit 68cc98cfd4
11 changed files with 116 additions and 100 deletions

View File

@@ -0,0 +1,18 @@
export function registerCategories(app, cache) {
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' });
}
});
}

View File

@@ -0,0 +1,17 @@
import path from 'path';
export function registerImages(app, cacheDir) {
app.get('/img/cat/:id.avif', (req, res) => {
const { id } = req.params;
const imagePath = path.join(cacheDir, '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');
}
});
});
}

View File

@@ -0,0 +1,20 @@
export function registerIndex(app, cache) {
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');
}
});
}

View File

@@ -0,0 +1,32 @@
export function registerProducts(app, cache, updateProductCache) {
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' });
}
});
}

113
src/server/server.js Normal file
View File

@@ -0,0 +1,113 @@
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';
import { registerCategories } from './routes/categories.js';
import { registerProducts } from './routes/products.js';
import { registerImages } from './routes/images.js';
import { registerIndex } from './routes/index.js';
import { registerConnection } from './socket/connection.js';
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 });
});
}
// Register socket connection handler
registerConnection(io);
// Register routes
registerCategories(app, cache);
registerProducts(app, cache, updateProductCache);
registerImages(app, CACHE_DIR);
registerIndex(app, cache);
httpServer.listen(PORT, HOST, () => {
console.log(`🌐 Server running on http://${HOST}:${PORT}`);
});
}

View File

@@ -0,0 +1,8 @@
export function registerConnection(io) {
io.on('connection', (socket) => {
console.log('🔌 Client connected');
socket.on('disconnect', () => {
console.log('🔌 Client disconnected');
});
});
}