UNPKG

@afterxleep/doc-bot

Version:

Generic MCP server for intelligent documentation access in any project

158 lines (135 loc) 4.08 kB
import { Worker } from 'worker_threads'; import os from 'os'; /** * ParallelSearchManager handles searching multiple docsets concurrently * using worker threads to improve performance with many docsets */ export class ParallelSearchManager { constructor(options = {}) { this.maxWorkers = options.maxWorkers || Math.min(os.cpus().length, 4); this.searchTimeout = options.searchTimeout || 2000; // 2 seconds per search this.cache = new Map(); this.cacheMaxSize = options.cacheMaxSize || 100; this.cacheMaxAge = options.cacheMaxAge || 5 * 60 * 1000; // 5 minutes } /** * Search multiple docsets in parallel using Promise.allSettled * This ensures one slow docset doesn't block all results */ async searchDocsetsParallel(databases, searchTerms, options = {}) { const { type, limit = 50 } = options; // Check cache first const cacheKey = this.getCacheKey(searchTerms, type); const cached = this.getFromCache(cacheKey); if (cached) { return cached; } // Prepare search promises with timeout const searchPromises = []; const limitPerDocset = Math.ceil(limit / Math.max(1, databases.size)); for (const db of databases.values()) { const searchPromise = this.searchWithTimeout( db.searchWithTerms(searchTerms, type, limitPerDocset), this.searchTimeout, db.docsetInfo.name ); searchPromises.push(searchPromise); } // Execute all searches in parallel const searchResults = await Promise.allSettled(searchPromises); // Collect successful results const allResults = []; let successCount = 0; let failureCount = 0; searchResults.forEach((result, index) => { if (result.status === 'fulfilled') { allResults.push(...result.value); successCount++; } else { failureCount++; console.warn(`Search failed for docset:`, result.reason); } }); // Sort by relevance score const sortedResults = allResults .sort((a, b) => b.relevanceScore - a.relevanceScore) .slice(0, limit); // Cache the results this.addToCache(cacheKey, sortedResults); // Add search metadata sortedResults._metadata = { totalDocsets: databases.size, successfulSearches: successCount, failedSearches: failureCount }; return sortedResults; } /** * Wrap search with timeout to prevent slow docsets from blocking */ async searchWithTimeout(searchPromise, timeout, docsetName) { return Promise.race([ searchPromise, new Promise((_, reject) => setTimeout(() => reject(new Error(`Search timeout for ${docsetName}`)), timeout) ) ]); } /** * Generate cache key from search parameters */ getCacheKey(searchTerms, type) { const termsKey = searchTerms.sort().join('|'); return `${termsKey}:${type || 'all'}`; } /** * Get cached results if still valid */ getFromCache(key) { const cached = this.cache.get(key); if (!cached) return null; const age = Date.now() - cached.timestamp; if (age > this.cacheMaxAge) { this.cache.delete(key); return null; } return cached.results; } /** * Add results to cache with LRU eviction */ addToCache(key, results) { // Implement simple LRU by deleting oldest entries if cache is full if (this.cache.size >= this.cacheMaxSize) { const firstKey = this.cache.keys().next().value; this.cache.delete(firstKey); } this.cache.set(key, { results, timestamp: Date.now() }); } /** * Clear the cache */ clearCache() { this.cache.clear(); } /** * Get cache statistics */ getCacheStats() { let totalAge = 0; let count = 0; for (const [key, value] of this.cache) { totalAge += Date.now() - value.timestamp; count++; } return { size: this.cache.size, maxSize: this.cacheMaxSize, averageAge: count > 0 ? totalAge / count : 0, maxAge: this.cacheMaxAge }; } }