UNPKG

crewai-ts

Version:

TypeScript port of crewAI for agent-based workflows

297 lines (296 loc) 10.6 kB
/** * IncrementalMemoryLoader implementation * Provides efficient loading of large datasets with minimal memory pressure * by loading only what's needed when it's needed */ /** * IncrementalMemoryLoader class * Implements efficient loading of large datasets with pagination and caching */ export class IncrementalMemoryLoader { // Function to fetch a page of items (supplied by consumer) fetchFn; // Configuration options pageSize; maxCachedPages; cacheStrategy; totalItems; keyFn; processItem; // Cache of loaded pages pageCache = new Map(); // LRU tracking for page eviction pageAccessOrder = []; // If using keyFn, we maintain a map from keys to page/index keyToLocationMap = new Map(); // State tracking for window mode windowStart = 0; windowEnd = 0; // Statistics for monitoring performance stats = { pageLoads: 0, pageEvictions: 0, cacheHits: 0, cacheMisses: 0 }; constructor(fetchFn, options = {}) { this.fetchFn = fetchFn; this.pageSize = options.pageSize ?? 100; this.maxCachedPages = options.maxCachedPages ?? 10; this.cacheStrategy = options.cacheStrategy ?? 'lru'; this.totalItems = options.totalItems; this.keyFn = options.keyFn; this.processItem = options.processItem; } /** * Get an item by its index */ async getItem(index) { if (this.totalItems !== undefined && (index < 0 || index >= this.totalItems)) { return undefined; } const pageIndex = Math.floor(index / this.pageSize); const indexInPage = index % this.pageSize; try { const page = await this.getPage(pageIndex); return page[indexInPage]; } catch (error) { console.error(`Error loading item at index ${index}:`, error); return undefined; } } /** * Get an item by its key (if keyFn was provided) */ async getItemByKey(key) { if (!this.keyFn) { throw new Error('keyFn must be provided to use getItemByKey'); } // Check if we already know where this item is const location = this.keyToLocationMap.get(key); if (location) { const { pageIndex, indexInPage } = location; const page = await this.getPage(pageIndex); return page[indexInPage]; } // We don't know where this item is, we need to search for it // This would typically require a separate index lookup mechanism // or scanning through pages until found, which is inefficient // A real implementation would have a proper index structure return undefined; } /** * Get a page of items by page index */ async getPage(pageIndex) { // Check if page is already in cache const cachedPage = this.pageCache.get(pageIndex); if (cachedPage) { // Update access time and order cachedPage.lastAccessed = Date.now(); this.updateAccessOrder(pageIndex); this.stats.cacheHits++; return cachedPage.items; } this.stats.cacheMisses++; // Fetch the page const offset = pageIndex * this.pageSize; const items = await this.fetchItems(offset, this.pageSize); // Process items if processor function is provided const processedItems = this.processItem ? items.map(item => this.processItem(item)) : items; // Create the page object const page = { items: processedItems, pageIndex, lastAccessed: Date.now() }; // Update key to location map if using keys if (this.keyFn) { processedItems.forEach((item, i) => { const key = this.keyFn(item); this.keyToLocationMap.set(key, { pageIndex, indexInPage: i }); }); } // Add to cache and update access order this.pageCache.set(pageIndex, page); this.pageAccessOrder.push(pageIndex); // Update window range if using window strategy if (this.cacheStrategy === 'window') { if (this.pageCache.size === 1) { // First page this.windowStart = pageIndex; this.windowEnd = pageIndex; } else { this.windowStart = Math.min(this.windowStart, pageIndex); this.windowEnd = Math.max(this.windowEnd, pageIndex); } } // Ensure cache doesn't exceed max size this.enforceMaxCacheSize(); this.stats.pageLoads++; return processedItems; } /** * Get a range of items */ async getRange(startIndex, endIndex) { if (startIndex < 0 || (this.totalItems !== undefined && startIndex >= this.totalItems)) { throw new Error(`Start index ${startIndex} out of bounds`); } if (endIndex < startIndex || (this.totalItems !== undefined && endIndex >= this.totalItems)) { endIndex = this.totalItems ? this.totalItems - 1 : endIndex; } const startPage = Math.floor(startIndex / this.pageSize); const endPage = Math.floor(endIndex / this.pageSize); // If range spans multiple pages, optimize by fetching all pages in parallel const pagePromises = []; for (let i = startPage; i <= endPage; i++) { pagePromises.push(this.getPage(i)); } const pages = await Promise.all(pagePromises); // Extract the requested range from the loaded pages const startIndexInPage = startIndex % this.pageSize; const endIndexInPage = endIndex % this.pageSize; if (startPage === endPage) { // Range is within a single page const page = pages[0]; if (!page) { return []; } return page.slice(startIndexInPage, endIndexInPage + 1); } else { // Range spans multiple pages const result = []; // First page (partial) if (pages[0]) { result.push(...pages[0].slice(startIndexInPage)); } // Middle pages (complete) for (let i = 1; i < pages.length - 1; i++) { const page = pages[i]; if (page) { result.push(...page); } } // Last page (partial) const lastPage = pages[pages.length - 1]; if (lastPage) { result.push(...lastPage.slice(0, endIndexInPage + 1)); } return result; } } /** * Reset the loader state and clear caches */ reset() { this.pageCache.clear(); this.pageAccessOrder = []; this.keyToLocationMap.clear(); this.windowStart = 0; this.windowEnd = 0; this.stats = { pageLoads: 0, pageEvictions: 0, cacheHits: 0, cacheMisses: 0 }; } /** * Get loader statistics */ getStats() { return { ...this.stats }; } /** * Update the access order of pages (for LRU tracking) */ updateAccessOrder(pageIndex) { // Remove the page from its current position this.pageAccessOrder = this.pageAccessOrder.filter(p => p !== pageIndex); // Add to the end (most recently used) this.pageAccessOrder.push(pageIndex); } /** * Ensure the cache doesn't exceed maximum size by evicting pages */ enforceMaxCacheSize() { if (this.pageCache.size <= this.maxCachedPages) { return; } const numToEvict = this.pageCache.size - this.maxCachedPages; if (this.cacheStrategy === 'lru') { // Evict least recently used pages const pagesToEvict = this.pageAccessOrder.slice(0, numToEvict); for (const pageIndex of pagesToEvict) { this.evictPage(pageIndex); } } else if (this.cacheStrategy === 'window') { // Evict pages outside the current window, prioritizing those furthest from window const allPages = Array.from(this.pageCache.keys()); // Calculate distance from window center for each page const windowStart = this.windowStart || 0; const windowEnd = this.windowEnd || 0; const windowCenter = (windowStart + windowEnd) / 2; const pagesWithDistance = allPages.map(pageIndex => { return { pageIndex, distance: Math.abs(pageIndex - windowCenter) }; }); // Sort by distance (descending) and evict furthest pages pagesWithDistance.sort((a, b) => b.distance - a.distance); for (let i = 0; i < numToEvict; i++) { if (i < pagesWithDistance.length) { const pageInfo = pagesWithDistance[i]; if (pageInfo) { this.evictPage(pageInfo.pageIndex); } } } } } /** * Evict a page from the cache */ evictPage(pageIndex) { // If using keys, remove the key mappings for this page if (this.keyFn) { const page = this.pageCache.get(pageIndex); if (page) { page.items.forEach((item, i) => { const key = this.keyFn(item); const location = this.keyToLocationMap.get(key); if (location && location.pageIndex === pageIndex && location.indexInPage === i) { this.keyToLocationMap.delete(key); } }); } } // Remove from cache and access order this.pageCache.delete(pageIndex); this.pageAccessOrder = this.pageAccessOrder.filter(p => p !== pageIndex); this.stats.pageEvictions++; } /** * Fetch items from the data source */ async fetchItems(offset, limit) { try { return await this.fetchFn(offset, limit); } catch (error) { console.error(`Error fetching items (offset: ${offset}, limit: ${limit}):`, error); return []; } } }