@synet/fs
Version:
Robust, battle-tested filesystem abstraction for Node.js
223 lines (222 loc) • 7.11 kB
JavaScript
"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);
}