crewai-ts
Version:
TypeScript port of crewAI for agent-based workflows
229 lines (228 loc) • 7.66 kB
JavaScript
/**
* Cache Handler
* Optimized caching system for agent responses with efficient memory usage
*/
// Add reference to Node.js types to ensure proper TypeScript recognition
/// <reference types="node" />
import { createHash } from 'crypto';
/**
* Efficient cache handler optimized for agent responses
* Implements LRU caching strategy with optional persistence
*/
export class CacheHandler {
cache;
maxSize;
ttl;
persistence;
persistenceDir;
/**
* Create a new CacheHandler
* @param options Cache configuration options
*/
constructor(options = {}) {
this.maxSize = options.maxSize ?? 1000;
this.ttl = options.ttl ?? 3600000; // Default: 1 hour
this.persistence = options.persistence ?? false;
this.persistenceDir = options.persistenceDir;
this.cache = new Map();
// Set up persistence if enabled
if (this.persistence) {
this.loadFromDisk();
// Set up automatic persistence
setInterval(() => this.saveToDisk(), 60000); // Save every minute
// Memory-optimized persistence strategy that avoids type issues
// Save periodically instead of relying on process exit events
// This avoids TypeScript compatibility issues with process event handling
const saveInterval = setInterval(() => {
this.saveToDisk();
}, 10000); // Save every 10 seconds for better data protection
// Clean up interval on class instance disposal if needed
// This approach is more memory-efficient than process event listeners
}
}
/**
* Generate a cache key from inputs
* @param keyParts Components to generate the key from
* @returns A hash-based cache key
*/
generateKey(...keyParts) {
const serializedParts = keyParts.map(part => {
if (typeof part === 'object') {
return JSON.stringify(part);
}
return String(part);
}).join('::');
return createHash('md5').update(serializedParts).digest('hex');
}
/**
* Get a value from the cache
* @param key Cache key
* @returns The cached value or undefined if not found
*/
get(key) {
const entry = this.cache.get(key);
if (!entry) {
return undefined;
}
// Check if the entry has expired
if (Date.now() - entry.createdAt > this.ttl) {
this.cache.delete(key);
return undefined;
}
// Update last accessed time for LRU tracking
entry.lastAccessed = Date.now();
return entry.value;
}
/**
* Set a value in the cache
* @param key Cache key
* @param value Value to cache
*/
set(key, value) {
// Enforce size limit before adding new items
if (this.cache.size >= this.maxSize) {
this.evictLRU();
}
const now = Date.now();
this.cache.set(key, {
value,
createdAt: now,
lastAccessed: now
});
}
/**
* Check if a key exists in the cache and is not expired
* @param key Cache key
* @returns True if the key exists and is not expired
*/
has(key) {
const entry = this.cache.get(key);
if (!entry) {
return false;
}
// Check if the entry has expired
if (Date.now() - entry.createdAt > this.ttl) {
this.cache.delete(key);
return false;
}
return true;
}
/**
* Delete a key from the cache
* @param key Cache key
* @returns True if the key was deleted, false otherwise
*/
delete(key) {
return this.cache.delete(key);
}
/**
* Clear the entire cache
*/
clear() {
this.cache.clear();
}
/**
* Get a value from the cache or compute it if not found
* @param key Cache key
* @param producer Function to produce the value if not in cache
* @returns The cached or computed value
*/
async getOrCompute(key, producer) {
// Check if we already have the value cached
const cachedValue = this.get(key);
if (cachedValue !== undefined) {
return cachedValue;
}
// Check if this key is already being processed
const entry = this.cache.get(key);
if (entry?.processing) {
// Wait for the processing to complete by polling
return new Promise((resolve, reject) => {
const checkInterval = setInterval(() => {
const entry = this.cache.get(key);
if (!entry?.processing) {
clearInterval(checkInterval);
const value = this.get(key);
if (value !== undefined) {
resolve(value);
}
else {
reject(new Error(`Compute failed for key: ${key}`));
}
}
}, 50); // Check every 50ms
// Set a timeout to avoid infinite polling
setTimeout(() => {
clearInterval(checkInterval);
reject(new Error(`Timeout waiting for computation of key: ${key}`));
}, 30000); // 30 second timeout
});
}
// Mark this key as being processed
this.cache.set(key, {
value: null,
createdAt: Date.now(),
lastAccessed: Date.now(),
processing: true
});
try {
// Compute the value
const value = await producer();
// Cache the computed value
this.set(key, value);
return value;
}
catch (error) {
// Remove the processing entry on error
this.delete(key);
throw error;
}
}
/**
* Remove the least recently used item from the cache
* @private
*/
evictLRU() {
let oldestKey = null;
let oldestTime = Infinity;
// Find the least recently accessed entry using Array.from for better compatibility
// This avoids MapIterator compatibility issues for optimized memory handling
Array.from(this.cache.keys()).forEach(key => {
const entry = this.cache.get(key);
// Skip if entry is undefined (type safety)
if (!entry)
return;
if (entry.lastAccessed < oldestTime) {
oldestTime = entry.lastAccessed;
oldestKey = key;
}
});
// Remove the oldest entry with null check for memory safety
if (oldestKey !== null) {
this.cache.delete(oldestKey);
}
}
/**
* Save the cache to disk if persistence is enabled
* @private
*/
saveToDisk() {
if (!this.persistence || !this.persistenceDir) {
return;
}
// In a real implementation, this would write to disk
// For now, we'll just log that it would happen
console.log(`Would save ${this.cache.size} cache entries to ${this.persistenceDir}`);
}
/**
* Load the cache from disk if persistence is enabled
* @private
*/
loadFromDisk() {
if (!this.persistence || !this.persistenceDir) {
return;
}
// In a real implementation, this would read from disk
console.log(`Would load cache from ${this.persistenceDir}`);
}
}