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
JavaScript
"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