feat: Enhance product management by adding WebSocket support for real-time updates, implement product loading in categories, and improve caching for category products.
This commit is contained in:
79
server.js
79
server.js
@@ -1,4 +1,6 @@
|
||||
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';
|
||||
@@ -6,8 +8,11 @@ import fs from 'fs/promises';
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
export function startServer(categorySyncer) {
|
||||
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';
|
||||
@@ -15,7 +20,8 @@ export function startServer(categorySyncer) {
|
||||
// Cache for ETags and data
|
||||
const cache = {
|
||||
categories: { etag: null, data: null },
|
||||
html: { etag: null, data: null }
|
||||
html: { etag: null, data: null },
|
||||
products: new Map() // id -> { etag, data }
|
||||
};
|
||||
|
||||
// Function to calculate ETag for categories
|
||||
@@ -34,6 +40,24 @@ export function startServer(categorySyncer) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
@@ -56,10 +80,27 @@ export function startServer(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 {
|
||||
@@ -78,6 +119,38 @@ export function startServer(categorySyncer) {
|
||||
}
|
||||
});
|
||||
|
||||
// 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;
|
||||
@@ -113,7 +186,7 @@ export function startServer(categorySyncer) {
|
||||
}
|
||||
});
|
||||
|
||||
app.listen(PORT, HOST, () => {
|
||||
httpServer.listen(PORT, HOST, () => {
|
||||
console.log(`🌐 Server running on http://${HOST}:${PORT}`);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user