UNPKG

@synet/fs

Version:

Robust, battle-tested filesystem abstraction for Node.js

223 lines (222 loc) 7.11 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.CachedFileSystem = void 0; exports.createCachedFileSystem = createCachedFileSystem; /** * LRU Cache with TTL support */ class LRUCache { constructor(maxSize, defaultTtl) { this.maxSize = maxSize; this.defaultTtl = defaultTtl; this.cache = new Map(); this.accessOrder = new Map(); this.accessCounter = 0; } set(key, value, ttl) { const entry = { data: value, timestamp: Date.now(), ttl: ttl ?? this.defaultTtl, }; // Remove existing entry if present if (this.cache.has(key)) { this.cache.delete(key); this.accessOrder.delete(key); } // Evict least recently used if at capacity if (this.cache.size >= this.maxSize) { this.evictLRU(); } this.cache.set(key, entry); this.accessOrder.set(key, ++this.accessCounter); } get(key) { const entry = this.cache.get(key); if (!entry) { return undefined; } // Check TTL if (Date.now() - entry.timestamp > entry.ttl) { this.cache.delete(key); this.accessOrder.delete(key); return undefined; } // Update access order this.accessOrder.set(key, ++this.accessCounter); return entry.data; } has(key) { return this.get(key) !== undefined; } delete(key) { const deleted = this.cache.delete(key); this.accessOrder.delete(key); return deleted; } clear() { this.cache.clear(); this.accessOrder.clear(); this.accessCounter = 0; } size() { return this.cache.size; } evictLRU() { let oldestKey; let oldestAccess = Number.POSITIVE_INFINITY; for (const [key, accessTime] of this.accessOrder.entries()) { if (accessTime < oldestAccess) { oldestAccess = accessTime; oldestKey = key; } } if (oldestKey) { this.cache.delete(oldestKey); this.accessOrder.delete(oldestKey); } } getStats() { return { size: this.cache.size, maxSize: this.maxSize, entries: Array.from(this.cache.keys()), }; } } /** * CachedFileSystem provides LRU caching with TTL for file operations * Dramatically improves performance by caching frequently accessed files */ class CachedFileSystem { constructor(baseFileSystem, options = {}) { this.baseFileSystem = baseFileSystem; this.options = { maxSize: options.maxSize ?? 100, ttl: options.ttl ?? 5 * 60 * 1000, // 5 minutes cacheExists: options.cacheExists ?? true, cacheDirListing: options.cacheDirListing ?? true, }; this.readCache = new LRUCache(this.options.maxSize, this.options.ttl); this.existsCache = new LRUCache(this.options.maxSize, this.options.ttl); this.dirCache = new LRUCache(this.options.maxSize, this.options.ttl); } /** * Get cache statistics */ getCacheStats() { return { readCache: this.readCache.getStats(), existsCache: this.existsCache.getStats(), dirCache: this.dirCache.getStats(), options: this.options, }; } /** * Clear all caches */ clearCache() { this.readCache.clear(); this.existsCache.clear(); this.dirCache.clear(); } /** * Invalidate cache for a specific file */ invalidateFile(path) { this.readCache.delete(path); this.existsCache.delete(path); // Also invalidate parent directory cache const parentDir = path.substring(0, path.lastIndexOf("/")) || "/"; this.dirCache.delete(parentDir); } /** * Invalidate cache for a directory and all its children */ invalidateDirectory(dirPath) { const normalizedDir = dirPath.endsWith("/") ? dirPath : `${dirPath}/`; // Clear directory listing cache this.dirCache.delete(dirPath); // Clear caches for all files in the directory for (const key of this.readCache.getStats().entries) { if (key.startsWith(normalizedDir)) { this.readCache.delete(key); this.existsCache.delete(key); } } } // IFileSystem implementation existsSync(path) { if (!this.options.cacheExists) { return this.baseFileSystem.existsSync(path); } const cached = this.existsCache.get(path); if (cached !== undefined) { return cached; } const exists = this.baseFileSystem.existsSync(path); this.existsCache.set(path, exists); return exists; } readFileSync(path) { const cached = this.readCache.get(path); if (cached !== undefined) { return cached; } const content = this.baseFileSystem.readFileSync(path); this.readCache.set(path, content); return content; } writeFileSync(path, data) { this.baseFileSystem.writeFileSync(path, data); // Update cache with new content this.readCache.set(path, data); this.existsCache.set(path, true); // Invalidate parent directory cache const parentDir = path.substring(0, path.lastIndexOf("/")) || "/"; this.dirCache.delete(parentDir); } deleteFileSync(path) { this.baseFileSystem.deleteFileSync(path); this.invalidateFile(path); } deleteDirSync(path) { this.baseFileSystem.deleteDirSync(path); this.invalidateDirectory(path); } readDirSync(dirPath) { if (!this.options.cacheDirListing) { return this.baseFileSystem.readDirSync(dirPath); } const cached = this.dirCache.get(dirPath); if (cached !== undefined) { return cached; } const entries = this.baseFileSystem.readDirSync(dirPath); this.dirCache.set(dirPath, entries); return entries; } ensureDirSync(path) { this.baseFileSystem.ensureDirSync(path); // Invalidate parent directory cache const parentDir = path.substring(0, path.lastIndexOf("/")) || "/"; this.dirCache.delete(parentDir); } chmodSync(path, mode) { this.baseFileSystem.chmodSync(path, mode); // Note: chmod doesn't affect file content or existence, so no cache invalidation needed } clear(dirPath) { if (this.baseFileSystem.clear) { this.baseFileSystem.clear(dirPath); this.invalidateDirectory(dirPath); } } } exports.CachedFileSystem = CachedFileSystem; /** * Convenience function to create a cached filesystem with common defaults */ function createCachedFileSystem(baseFileSystem, options) { return new CachedFileSystem(baseFileSystem, options); }