@aaswe/codebase-ai
Version:
AI-Assisted Software Engineering (AASWE) - Rich codebase context for IDE LLMs
488 lines • 17.4 kB
JavaScript
"use strict";
/**
* Cache Manager
*
* Implements intelligent caching strategies for the Hybrid Storage Manager
* with support for multiple eviction policies, TTL management, and persistence.
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.CacheManager = void 0;
const events_1 = require("events");
const fs_1 = require("fs");
const path = __importStar(require("path"));
const logger_1 = __importDefault(require("../../../utils/logger"));
const types_1 = require("./types");
class CacheManager extends events_1.EventEmitter {
config;
cache = new Map();
accessOrder = []; // For LRU
accessCount = new Map(); // For LFU
timers = new Map(); // For TTL cleanup
metricsCollectionTimer;
persistenceTimer;
metrics;
hits = 0;
misses = 0;
constructor(config) {
super();
this.config = config;
this.metrics = this.initializeMetrics();
this.setupMetricsCollection();
this.setupPersistence();
}
/**
* Initialize the cache manager
*/
async initialize() {
try {
logger_1.default.info('Initializing Cache Manager');
if (this.config.persistenceEnabled && this.config.persistenceFile) {
await this.loadFromPersistence();
}
logger_1.default.info('Cache Manager initialized successfully');
this.emit('initialized');
}
catch (error) {
logger_1.default.error('Failed to initialize Cache Manager:', error);
throw error;
}
}
/**
* Get value from cache
*/
async get(key) {
const entry = this.cache.get(key);
if (!entry) {
this.misses++;
this.metrics.missRate = this.calculateMissRate();
this.emit('cache_miss', { key });
return null;
}
// Check TTL expiration
if (this.isExpired(entry)) {
await this.delete(key);
this.misses++;
this.metrics.missRate = this.calculateMissRate();
this.emit('cache_miss', { key, reason: 'expired' });
return null;
}
// Update access tracking
this.updateAccessTracking(key, entry);
this.hits++;
this.metrics.hitRate = this.calculateHitRate();
this.emit('cache_hit', { key });
return entry.value;
}
/**
* Set value in cache
*/
async set(key, value, ttl, tags) {
try {
// Check if we need to evict entries
if (this.cache.size >= this.config.maxEntries) {
await this.evictEntries(1);
}
// Calculate entry size
const size = this.calculateSize(value);
// Check memory limits
if (this.getTotalSize() + size > this.config.maxMemoryMB * 1024 * 1024) {
await this.evictByMemory(size);
}
const effectiveTTL = ttl || this.config.defaultTTL;
const entry = {
key,
value,
ttl: effectiveTTL,
createdAt: new Date(),
accessCount: 1,
lastAccessed: new Date(),
size,
...(tags && { tags })
};
// Remove existing entry if present
if (this.cache.has(key)) {
await this.delete(key);
}
// Add new entry
this.cache.set(key, entry);
this.accessOrder.push(key);
this.accessCount.set(key, 1);
// Set TTL timer
if (effectiveTTL > 0) {
const timer = setTimeout(() => {
this.delete(key).catch(error => {
logger_1.default.error(`Failed to delete expired cache entry ${key}:`, error);
});
}, effectiveTTL);
this.timers.set(key, timer);
}
this.updateMetrics();
this.emit('cache_set', { key, size, ttl: effectiveTTL });
}
catch (error) {
logger_1.default.error(`Failed to set cache entry ${key}:`, error);
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
const errorObj = error instanceof Error ? error : undefined;
throw new types_1.CacheError(`Failed to set cache entry: ${errorMessage}`, key, errorObj);
}
}
/**
* Delete entry from cache
*/
async delete(key) {
const entry = this.cache.get(key);
if (!entry) {
return false;
}
// Clear TTL timer
const timer = this.timers.get(key);
if (timer) {
clearTimeout(timer);
this.timers.delete(key);
}
// Remove from tracking structures
this.cache.delete(key);
this.accessCount.delete(key);
const orderIndex = this.accessOrder.indexOf(key);
if (orderIndex !== -1) {
this.accessOrder.splice(orderIndex, 1);
}
this.updateMetrics();
this.emit('cache_delete', { key });
return true;
}
/**
* Clear cache entries by pattern or tags
*/
async invalidate(pattern, tags) {
let deletedCount = 0;
const keysToDelete = [];
for (const [key, entry] of this.cache) {
let shouldDelete = false;
// Check pattern match
if (pattern) {
const regex = new RegExp(pattern);
shouldDelete = regex.test(key);
}
// Check tag match
if (tags && entry.tags) {
shouldDelete = shouldDelete || tags.some(tag => entry.tags.includes(tag));
}
// If no pattern or tags specified, delete all
if (!pattern && !tags) {
shouldDelete = true;
}
if (shouldDelete) {
keysToDelete.push(key);
}
}
// Delete matched entries
for (const key of keysToDelete) {
if (await this.delete(key)) {
deletedCount++;
}
}
this.emit('cache_invalidate', { pattern, tags, deletedCount });
return deletedCount;
}
/**
* Get cache metrics
*/
getMetrics() {
return { ...this.metrics };
}
/**
* Get cache statistics
*/
getStats() {
return {
totalEntries: this.cache.size,
totalSize: this.getTotalSize(),
memoryUsageMB: this.getTotalSize() / (1024 * 1024),
hitRate: this.metrics.hitRate,
missRate: this.metrics.missRate,
evictionRate: this.metrics.evictionRate,
averageAccessTime: this.metrics.averageAccessTime,
oldestEntry: this.getOldestEntry(),
newestEntry: this.getNewestEntry()
};
}
/**
* Shutdown the cache manager
*/
async shutdown() {
try {
logger_1.default.info('Shutting down Cache Manager');
// Clear all timers
for (const timer of this.timers.values()) {
clearTimeout(timer);
}
this.timers.clear();
if (this.metricsCollectionTimer) {
clearInterval(this.metricsCollectionTimer);
this.metricsCollectionTimer = undefined;
}
if (this.persistenceTimer) {
clearInterval(this.persistenceTimer);
this.persistenceTimer = undefined;
}
// Save to persistence if enabled
if (this.config.persistenceEnabled) {
await this.saveToPersistence();
}
// Clear cache
this.cache.clear();
this.accessOrder.length = 0;
this.accessCount.clear();
logger_1.default.info('Cache Manager shutdown completed');
this.emit('shutdown');
}
catch (error) {
logger_1.default.error('Cache Manager shutdown failed:', error);
throw error;
}
}
// Private helper methods
initializeMetrics() {
return {
hitRate: 0,
missRate: 0,
evictionRate: 0,
totalEntries: 0,
totalSize: 0,
averageAccessTime: 0,
memoryUsage: 0
};
}
setupMetricsCollection() {
this.metricsCollectionTimer = setInterval(() => {
this.updateMetrics();
}, 10000); // Update every 10 seconds
// Ensure timer doesn't keep Node.js process alive
this.metricsCollectionTimer.unref();
}
setupPersistence() {
if (this.config.persistenceEnabled && this.config.persistenceFile) {
this.persistenceTimer = setInterval(() => {
this.saveToPersistence().catch(error => {
logger_1.default.error('Failed to save cache to persistence:', error);
});
}, 60000); // Save every minute
// Ensure timer doesn't keep Node.js process alive
this.persistenceTimer.unref();
}
}
updateAccessTracking(key, entry) {
entry.accessCount++;
entry.lastAccessed = new Date();
// Update access count tracking
this.accessCount.set(key, entry.accessCount);
// Update LRU order
const orderIndex = this.accessOrder.indexOf(key);
if (orderIndex !== -1) {
this.accessOrder.splice(orderIndex, 1);
}
this.accessOrder.push(key);
}
isExpired(entry) {
if (entry.ttl <= 0)
return false;
return Date.now() - entry.createdAt.getTime() > entry.ttl;
}
calculateSize(value) {
try {
return JSON.stringify(value).length * 2; // Rough estimate (UTF-16)
}
catch {
return 1024; // Default size for non-serializable objects
}
}
getTotalSize() {
let totalSize = 0;
for (const entry of this.cache.values()) {
totalSize += entry.size;
}
return totalSize;
}
async evictEntries(count) {
const keysToEvict = this.selectEvictionCandidates(count);
for (const key of keysToEvict) {
await this.delete(key);
this.metrics.evictionRate++;
}
this.emit('cache_eviction', { count: keysToEvict.length, policy: this.config.evictionPolicy });
}
async evictByMemory(requiredSize) {
let freedSize = 0;
const keysToEvict = [];
while (freedSize < requiredSize && this.cache.size > 0) {
const candidates = this.selectEvictionCandidates(1);
if (candidates.length === 0)
break;
const key = candidates[0];
const entry = this.cache.get(key);
if (entry) {
freedSize += entry.size;
keysToEvict.push(key);
}
}
for (const key of keysToEvict) {
await this.delete(key);
this.metrics.evictionRate++;
}
}
selectEvictionCandidates(count) {
const candidates = [];
switch (this.config.evictionPolicy) {
case 'lru':
candidates.push(...this.accessOrder.slice(0, count));
break;
case 'lfu':
const sortedByFrequency = Array.from(this.accessCount.entries())
.sort(([, a], [, b]) => a - b)
.slice(0, count)
.map(([key]) => key);
candidates.push(...sortedByFrequency);
break;
case 'ttl':
const sortedByAge = Array.from(this.cache.entries())
.sort(([, a], [, b]) => a.createdAt.getTime() - b.createdAt.getTime())
.slice(0, count)
.map(([key]) => key);
candidates.push(...sortedByAge);
break;
case 'random':
const keys = Array.from(this.cache.keys());
for (let i = 0; i < Math.min(count, keys.length); i++) {
const randomIndex = Math.floor(Math.random() * keys.length);
candidates.push(keys[randomIndex]);
}
break;
}
return candidates;
}
calculateHitRate() {
const totalRequests = this.hits + this.misses;
if (totalRequests === 0) {
return 0;
}
return this.hits / totalRequests;
}
calculateMissRate() {
return 1 - this.calculateHitRate();
}
updateMetrics() {
this.metrics.totalEntries = this.cache.size;
this.metrics.totalSize = this.getTotalSize();
this.metrics.memoryUsage = this.metrics.totalSize / (this.config.maxMemoryMB * 1024 * 1024);
// Calculate average access time (simplified)
let totalAccessTime = 0;
let accessCount = 0;
for (const entry of this.cache.values()) {
totalAccessTime += entry.lastAccessed.getTime() - entry.createdAt.getTime();
accessCount += entry.accessCount;
}
this.metrics.averageAccessTime = accessCount > 0 ? totalAccessTime / accessCount : 0;
}
getOldestEntry() {
let oldest = null;
for (const entry of this.cache.values()) {
if (!oldest || entry.createdAt < oldest) {
oldest = entry.createdAt;
}
}
return oldest;
}
getNewestEntry() {
let newest = null;
for (const entry of this.cache.values()) {
if (!newest || entry.createdAt > newest) {
newest = entry.createdAt;
}
}
return newest;
}
async loadFromPersistence() {
if (!this.config.persistenceFile)
return;
try {
const data = await fs_1.promises.readFile(this.config.persistenceFile, 'utf-8');
const persistedData = JSON.parse(data);
for (const entryData of persistedData.entries || []) {
const entry = {
...entryData,
createdAt: new Date(entryData.createdAt),
lastAccessed: new Date(entryData.lastAccessed)
};
// Check if entry is still valid
if (!this.isExpired(entry)) {
this.cache.set(entry.key, entry);
this.accessOrder.push(entry.key);
this.accessCount.set(entry.key, entry.accessCount);
}
}
logger_1.default.info(`Loaded ${this.cache.size} entries from cache persistence`);
}
catch (error) {
if (error.code !== 'ENOENT') {
logger_1.default.error('Failed to load cache from persistence:', error);
}
}
}
async saveToPersistence() {
if (!this.config.persistenceFile)
return;
try {
const persistenceDir = path.dirname(this.config.persistenceFile);
await fs_1.promises.mkdir(persistenceDir, { recursive: true });
const persistedData = {
timestamp: new Date().toISOString(),
entries: Array.from(this.cache.values())
};
await fs_1.promises.writeFile(this.config.persistenceFile, JSON.stringify(persistedData, null, 2), 'utf-8');
logger_1.default.debug(`Saved ${this.cache.size} entries to cache persistence`);
}
catch (error) {
logger_1.default.error('Failed to save cache to persistence:', error);
}
}
}
exports.CacheManager = CacheManager;
exports.default = CacheManager;
//# sourceMappingURL=CacheManager.js.map