UNPKG

jexidb

Version:

JexiDB is a pure JS NPM library for managing data on disk efficiently, without the need for a server.

286 lines (253 loc) 8.39 kB
/** * TermManager - Manages term-to-ID mapping for efficient storage * * Responsibilities: * - Map terms to numeric IDs for space efficiency * - Track term usage counts for cleanup * - Load/save terms from/to index file * - Clean up orphaned terms */ export default class TermManager { constructor() { this.termToId = new Map() // "bra" -> 1 this.idToTerm = new Map() // 1 -> "bra" this.termCounts = new Map() // 1 -> 1500 (how many times used) this.nextId = 1 } /** * Get ID for a term (create if doesn't exist) * @param {string} term - Term to get ID for * @returns {number} - Numeric ID for the term */ getTermId(term) { if (this.termToId.has(term)) { const id = this.termToId.get(term) this.termCounts.set(id, (this.termCounts.get(id) || 0) + 1) return id } const id = this.nextId++ this.termToId.set(term, id) this.idToTerm.set(id, term) this.termCounts.set(id, 1) return id } /** * Get term ID without incrementing count (for IndexManager use) * @param {string} term - Term to get ID for * @returns {number|undefined} - Numeric ID for the term, or undefined if not found * CRITICAL: Does NOT create new IDs - only returns existing ones * This prevents creating invalid term IDs during queries when terms haven't been loaded yet */ getTermIdWithoutIncrement(term) { if (this.termToId.has(term)) { return this.termToId.get(term) } // CRITICAL FIX: Don't create new IDs during queries // If term doesn't exist, return undefined // This ensures queries only work with terms that were actually saved to the database return undefined } /** * Get term by ID * @param {number} id - Numeric ID * @returns {string|null} - Term or null if not found */ getTerm(id) { return this.idToTerm.get(id) || null } /** * Bulk get term IDs for multiple terms (optimized for performance) * @param {string[]} terms - Array of terms to get IDs for * @returns {number[]} - Array of term IDs in the same order */ bulkGetTermIds(terms) { if (!Array.isArray(terms) || terms.length === 0) { return [] } const termIds = new Array(terms.length) // Process all terms in a single pass for (let i = 0; i < terms.length; i++) { const term = terms[i] if (this.termToId.has(term)) { const id = this.termToId.get(term) this.termCounts.set(id, (this.termCounts.get(id) || 0) + 1) termIds[i] = id } else { const id = this.nextId++ this.termToId.set(term, id) this.idToTerm.set(id, term) this.termCounts.set(id, 1) termIds[i] = id } } return termIds } /** * Load terms from file data * @param {Object} termsData - Terms data from file */ loadTerms(termsData) { if (!termsData || typeof termsData !== 'object') { return } for (const [id, term] of Object.entries(termsData)) { const numericId = parseInt(id) if (!isNaN(numericId) && term) { this.termToId.set(term, numericId) this.idToTerm.set(numericId, term) this.nextId = Math.max(this.nextId, numericId + 1) // Initialize count to 0 - will be updated as terms are used this.termCounts.set(numericId, 0) } } } /** * Save terms to file format * @returns {Object} - Terms data for file */ saveTerms() { const termsData = {} for (const [id, term] of this.idToTerm) { termsData[id] = term } return termsData } /** * Clean up orphaned terms (terms with count 0) * @param {boolean} forceCleanup - Force cleanup even if conditions not met * @param {Object} options - Cleanup options * @returns {number} - Number of orphaned terms removed */ cleanupOrphanedTerms(forceCleanup = false, options = {}) { const { intelligentCleanup = true, minOrphanCount = 10, orphanPercentage = 0.15, checkSystemState = true } = options // INTELLIGENT CLEANUP: Check if cleanup should be performed if (!forceCleanup && intelligentCleanup) { const stats = this.getStats() const orphanedCount = stats.orphanedTerms const totalTerms = stats.totalTerms // SAFETY: If all terms are marked as orphaned, it likely means counts // haven't been rebuilt after loading from disk. Skip cleanup to avoid // wiping valid term mappings. if (totalTerms > 0 && orphanedCount === totalTerms) { return 0 } // Only cleanup if conditions are met const shouldCleanup = ( orphanedCount >= minOrphanCount && // Minimum orphan count orphanedCount > totalTerms * orphanPercentage && // Orphans > percentage of total (!checkSystemState || this.isSystemSafe()) // System is safe (if check enabled) ) if (!shouldCleanup) { return 0 // Don't cleanup if conditions not met } } else if (!forceCleanup) { return 0 // Don't remove anything during normal operations } // PERFORM CLEANUP: Remove orphaned terms const orphanedIds = [] for (const [id, count] of this.termCounts) { if (count === 0) { orphanedIds.push(id) } } // Remove orphaned terms with additional safety checks for (const id of orphanedIds) { const term = this.idToTerm.get(id) if (term && typeof term === 'string') { // Extra safety: only remove string terms this.termToId.delete(term) this.idToTerm.delete(id) this.termCounts.delete(id) } } return orphanedIds.length } /** * Check if system is safe for cleanup operations * @returns {boolean} - True if system is safe for cleanup */ isSystemSafe() { // This method should be overridden by the database instance // to provide system state information return true // Default to safe for backward compatibility } /** * Perform intelligent automatic cleanup * @param {Object} options - Cleanup options * @returns {number} - Number of orphaned terms removed */ performIntelligentCleanup(options = {}) { return this.cleanupOrphanedTerms(false, { intelligentCleanup: true, minOrphanCount: 5, // Lower threshold for automatic cleanup orphanPercentage: 0.1, // 10% of total terms checkSystemState: true, ...options }) } /** * Decrement term count (when term is removed from index) * @param {number} termId - Term ID to decrement */ decrementTermCount(termId) { const count = this.termCounts.get(termId) || 0 this.termCounts.set(termId, Math.max(0, count - 1)) } /** * Increment term count (when term is added to index) * @param {number} termId - Term ID to increment */ incrementTermCount(termId) { const count = this.termCounts.get(termId) || 0 this.termCounts.set(termId, count + 1) } /** * Get statistics about terms * @returns {Object} - Term statistics */ getStats() { return { totalTerms: this.termToId.size, nextId: this.nextId, orphanedTerms: Array.from(this.termCounts.entries()).filter(([_, count]) => count === 0).length } } /** * Check if a term exists * @param {string} term - Term to check * @returns {boolean} - True if term exists */ hasTerm(term) { return this.termToId.has(term) } /** * Get all terms * @returns {Array} - Array of all terms */ getAllTerms() { return Array.from(this.termToId.keys()) } /** * Get all term IDs * @returns {Array} - Array of all term IDs */ getAllTermIds() { return Array.from(this.idToTerm.keys()) } /** * Get statistics about term mapping * @returns {Object} - Statistics object */ getStatistics() { return { totalTerms: this.termToId.size, nextId: this.nextId, termCounts: Object.fromEntries(this.termCounts), sampleTerms: Array.from(this.termToId.entries()).slice(0, 5) } } }