UNPKG

axiodb

Version:

The Pure JavaScript Alternative to SQLite. Embedded NoSQL database for Node.js with MongoDB-style queries, zero native dependencies, built-in InMemoryCache, and web GUI. Perfect for desktop apps, CLI tools, and embedded systems. No compilation, no platfor

286 lines 11.4 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.IndexCache = void 0; /* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-explicit-any */ const Keys_1 = require("../../config/Keys/Keys"); const FileManager_1 = __importDefault(require("../../engine/Filesystem/FileManager")); const Converter_helper_1 = __importDefault(require("../../Helper/Converter.helper")); /** * In-memory cache for index data * * Features: * - Eagerly loads all indexes on collection initialization * - Keeps indexes in both memory (speed) and disk (persistence) * - Cold start recovery: Loads from disk on cache miss * - Thread-safe with simple lock mechanism * - Dual-write: Updates both memory and disk atomically * * @example * ```typescript * const indexCache = new IndexCache('/path/to/collection'); * await indexCache.loadAllIndexes(); // Eager load * const indexData = await indexCache.getIndex('email'); // O(1) memory access * ``` */ class IndexCache { constructor(collectionPath) { this.cleanupInterval = null; this.cache = new Map(); this.indexFolderPath = `${collectionPath}/indexes`; this.indexMetaPath = `${this.indexFolderPath}/index.meta.json`; this.fileManager = new FileManager_1.default(); this.converter = new Converter_helper_1.default(); this.locks = new Map(); this.startCleanupInterval(); } /** * Generates a random TTL between 5-15 minutes * Randomization prevents cache stampede (thundering herd problem) */ generateRandomTTL() { return Math.floor(Math.random() * (IndexCache.MAX_TTL_MS - IndexCache.MIN_TTL_MS + 1) + IndexCache.MIN_TTL_MS); } /** * Starts periodic cleanup of expired cache entries */ startCleanupInterval() { if (this.cleanupInterval) return; this.cleanupInterval = setInterval(() => { this.cleanupExpiredEntries(); }, IndexCache.CLEANUP_INTERVAL_MS); // Ensure cleanup doesn't prevent process exit if (this.cleanupInterval.unref) { this.cleanupInterval.unref(); } } /** * Removes all expired entries from cache */ cleanupExpiredEntries() { const now = Date.now(); for (const [fieldName, cached] of this.cache.entries()) { if (now >= cached.expiresAt) { this.cache.delete(fieldName); } } } /** * Checks if a cached entry is expired */ isExpired(cached) { return Date.now() >= cached.expiresAt; } /** * Eagerly loads all indexes into memory * Called during collection initialization for maximum query performance * * @returns Promise that resolves when all indexes are loaded */ loadAllIndexes() { return __awaiter(this, void 0, void 0, function* () { try { // Check if index metadata exists const metaExists = yield this.fileManager.FileExists(this.indexMetaPath); if (!metaExists.status) { return; // No indexes created yet } // Read index metadata file const metaContent = yield this.fileManager.ReadFile(this.indexMetaPath); if (!metaContent.status) { return; } const indexMeta = this.converter.ToObject(metaContent.data); // Load each index file into memory in parallel const loadPromises = indexMeta.map((meta) => __awaiter(this, void 0, void 0, function* () { try { const indexPath = meta.path; const indexContent = yield this.fileManager.ReadFile(indexPath); if (indexContent.status) { const indexData = this.converter.ToObject(indexContent.data); this.cache.set(meta.indexFieldName, { data: indexData, loadedAt: new Date(), expiresAt: Date.now() + this.generateRandomTTL(), path: indexPath, }); } } catch (error) { // Silent per-index failure - continue loading other indexes console.error(`Failed to load index ${meta.indexFieldName}:`, error); } })); yield Promise.all(loadPromises); } catch (error) { // Silent failure - indexes will load from disk on demand (cold start recovery) console.error("Failed to load indexes into memory:", error); } }); } /** * Gets index data for a specific field * Returns from memory if available, loads from disk if not (cold start recovery) * * @param fieldName - The indexed field name (e.g., 'email', 'age') * @returns Index data or null if index doesn't exist */ getIndex(fieldName) { return __awaiter(this, void 0, void 0, function* () { // Try memory cache first (O(1) fast path) const cached = this.cache.get(fieldName); if (cached) { // Check if entry is expired if (this.isExpired(cached)) { this.cache.delete(fieldName); // Fall through to reload from disk } else { return cached.data; } } // Cache miss or expired - load from disk (cold start recovery) try { const indexPath = `${this.indexFolderPath}/${fieldName}${Keys_1.General.DBMS_File_EXT}`; const indexContent = yield this.fileManager.ReadFile(indexPath); if (indexContent.status) { try { const indexData = this.converter.ToObject(indexContent.data); // Populate cache for future reads with random TTL this.cache.set(fieldName, { data: indexData, loadedAt: new Date(), expiresAt: Date.now() + this.generateRandomTTL(), path: indexPath, }); return indexData; } catch (parseError) { // JSON parse failed - index file may be corrupted, return null console.error(`Index file corrupted for ${fieldName}, skipping cache`); return null; } } } catch (error) { // Index doesn't exist - this is normal for unindexed fields } return null; }); } /** * Updates an index in both memory and disk atomically * Thread-safe with simple lock mechanism * * @param fieldName - The indexed field name * @param indexData - The updated index data * @returns True if update successful, false otherwise */ updateIndex(fieldName, indexData) { return __awaiter(this, void 0, void 0, function* () { // Acquire lock for this field to prevent concurrent writes yield this.acquireLock(fieldName); try { const indexPath = `${this.indexFolderPath}/${fieldName}${Keys_1.General.DBMS_File_EXT}`; // Write to disk first for durability (disk = source of truth) const writeResult = yield this.fileManager.WriteFile(indexPath, this.converter.ToString(indexData)); if (!writeResult.status) { return false; } // Update memory cache after successful disk write with fresh TTL this.cache.set(fieldName, { data: indexData, loadedAt: new Date(), expiresAt: Date.now() + this.generateRandomTTL(), path: indexPath, }); return true; } finally { this.releaseLock(fieldName); } }); } /** * Invalidates a specific index (removes from memory) * Disk copy remains for persistence and recovery * * @param fieldName - The indexed field name to invalidate */ invalidateIndex(fieldName) { return __awaiter(this, void 0, void 0, function* () { this.cache.delete(fieldName); }); } /** * Invalidates all indexes (removes all from memory) * Used when indexes are dropped or collection is cleared * Disk copies remain for recovery */ invalidateAll() { return __awaiter(this, void 0, void 0, function* () { this.cache.clear(); }); } /** * Simple lock acquisition for thread safety * Waits if another operation is currently holding the lock * * @param key - The lock key (typically field name) * @private */ acquireLock(key) { return __awaiter(this, void 0, void 0, function* () { const existingLock = this.locks.get(key); if (existingLock) { yield existingLock; } // Create new lock promise for this operation let releaseFn; const lockPromise = new Promise((resolve) => { releaseFn = resolve; }); this.locks.set(key, lockPromise); }); } /** * Releases the lock for a specific key * * @param key - The lock key to release * @private */ releaseLock(key) { this.locks.delete(key); } /** * Gets current cache statistics for monitoring * * @returns Object containing cache size and loaded index count */ getCacheStats() { return { indexCount: this.cache.size, fieldNames: Array.from(this.cache.keys()), }; } } exports.IndexCache = IndexCache; // TTL constants (5-15 minutes in milliseconds) IndexCache.MIN_TTL_MS = 5 * 60 * 1000; // 5 minutes IndexCache.MAX_TTL_MS = 15 * 60 * 1000; // 15 minutes IndexCache.CLEANUP_INTERVAL_MS = 60 * 1000; // Check every 1 minute //# sourceMappingURL=IndexCache.service.js.map