feat: Implement server-side category and product search via WebSockets, replacing client-side filtering with debounced input and dynamic tree expansion based on server results.
This commit is contained in:
@@ -99,7 +99,7 @@ export function startServer(categorySyncer, categoryProductsSyncer) {
|
||||
}
|
||||
|
||||
// Register socket connection handler
|
||||
registerConnection(io);
|
||||
registerConnection(io, CACHE_DIR);
|
||||
|
||||
// Register routes
|
||||
registerCategories(app, cache);
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
export function registerConnection(io) {
|
||||
import { findMatches } from '../utils/search-helper.js';
|
||||
|
||||
export function registerConnection(io, cacheDir) {
|
||||
io.on('connection', (socket) => {
|
||||
console.log('🔌 Client connected');
|
||||
|
||||
socket.on('search', async (query) => {
|
||||
// console.log(`🔍 Search request: "${query}"`);
|
||||
try {
|
||||
const matches = await findMatches(query, cacheDir);
|
||||
socket.emit('searchResults', { query, matches });
|
||||
} catch (err) {
|
||||
console.error('Search error:', err);
|
||||
socket.emit('searchResults', { query, matches: [] });
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
console.log('🔌 Client disconnected');
|
||||
});
|
||||
|
||||
73
src/server/utils/search-helper.js
Normal file
73
src/server/utils/search-helper.js
Normal file
@@ -0,0 +1,73 @@
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
|
||||
export async function findMatches(query, cacheDir) {
|
||||
if (!query || !query.trim()) return [];
|
||||
const terms = query.toLowerCase().split(/\s+/).filter(t => t);
|
||||
|
||||
// Load category tree
|
||||
const treePath = path.join(cacheDir, 'category_tree.json');
|
||||
let tree = [];
|
||||
try {
|
||||
const treeData = await fs.readFile(treePath, 'utf-8');
|
||||
tree = JSON.parse(treeData);
|
||||
} catch (e) {
|
||||
console.error("Failed to load category tree for search", e);
|
||||
return [];
|
||||
}
|
||||
|
||||
const matchingCategoryIds = new Set();
|
||||
|
||||
// Helper to check text match
|
||||
const isMatch = (text) => {
|
||||
if (!text) return false;
|
||||
const lower = text.toLowerCase();
|
||||
return terms.every(t => lower.includes(t));
|
||||
};
|
||||
|
||||
// Flatten tree to linear list for easier iteration
|
||||
const queue = [...tree];
|
||||
const nodes = [];
|
||||
while (queue.length > 0) {
|
||||
const node = queue.shift();
|
||||
nodes.push(node);
|
||||
if (node.children) {
|
||||
queue.push(...node.children);
|
||||
}
|
||||
}
|
||||
|
||||
// Process in chunks to avoid too many open files
|
||||
const CHUNK_SIZE = 50;
|
||||
for (let i = 0; i < nodes.length; i += CHUNK_SIZE) {
|
||||
const chunk = nodes.slice(i, i + CHUNK_SIZE);
|
||||
await Promise.all(chunk.map(async (node) => {
|
||||
let nodeMatches = false;
|
||||
|
||||
// Check category name
|
||||
if (isMatch(node.cName)) {
|
||||
nodeMatches = true;
|
||||
} else {
|
||||
// Check products
|
||||
try {
|
||||
const prodPath = path.join(cacheDir, 'products', `category_${node.kKategorie}.json`);
|
||||
// Check if file exists before reading to avoid throwing too many errors
|
||||
// Actually readFile throws ENOENT which is fine
|
||||
const prodData = await fs.readFile(prodPath, 'utf-8');
|
||||
const products = JSON.parse(prodData);
|
||||
|
||||
if (products.some(p => isMatch(p.cName))) {
|
||||
nodeMatches = true;
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore missing files
|
||||
}
|
||||
}
|
||||
|
||||
if (nodeMatches) {
|
||||
matchingCategoryIds.add(node.kKategorie);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
return Array.from(matchingCategoryIds);
|
||||
}
|
||||
Reference in New Issue
Block a user