2025-08-26 12:32:21 -07:00
/ * *
* Multi - level Cache Manager
*
* Implements a three - level caching strategy :
* - Level 1 : Hot cache ( most accessed nodes ) - RAM ( automatically detecting and adjusting in each environment )
* - Level 2 : Warm cache ( recent nodes ) - OPFS , Filesystem or S3 depending on environment
* - Level 3 : Cold storage ( all nodes ) - OPFS , Filesystem or S3 depending on environment
* /
import { HNSWNoun , GraphVerb , HNSWVerb } from '../coreTypes.js'
import { BrainyError } from '../errors/brainyError.js'
// Extend Navigator interface to include deviceMemory property
// and WorkerGlobalScope to include storage property
declare global {
interface Navigator {
deviceMemory? : number ;
}
interface WorkerGlobalScope {
storage ? : {
getDirectory ? : ( ) = > Promise < any > ;
[ key : string ] : any ;
} ;
}
}
// Type aliases for better readability
type HNSWNode = HNSWNoun
type Edge = GraphVerb
// Cache entry with metadata for LRU and TTL management
interface CacheEntry < T > {
data : T
lastAccessed : number
accessCount : number
expiresAt : number | null
}
// Cache statistics for monitoring and tuning
interface CacheStats {
hits : number
misses : number
evictions : number
size : number
maxSize : number
hotCacheSize : number
warmCacheSize : number
hotCacheHits : number
hotCacheMisses : number
warmCacheHits : number
warmCacheMisses : number
}
// Environment detection for storage selection
enum Environment {
BROWSER ,
NODE ,
WORKER
}
// Storage type for warm and cold caches
enum StorageType {
MEMORY ,
OPFS ,
FILESYSTEM ,
S3 ,
REMOTE_API
}
/ * *
* Multi - level cache manager for efficient data access
* /
export class CacheManager < T extends HNSWNode | Edge | HNSWVerb > {
// Hot cache (RAM)
private hotCache = new Map < string , CacheEntry < T > > ( )
// Cache statistics
private stats : CacheStats = {
hits : 0 ,
misses : 0 ,
evictions : 0 ,
size : 0 ,
maxSize : 0 ,
hotCacheSize : 0 ,
warmCacheSize : 0 ,
hotCacheHits : 0 ,
hotCacheMisses : 0 ,
warmCacheHits : 0 ,
warmCacheMisses : 0
}
// Environment and storage configuration
private environment : Environment
private warmStorageType : StorageType
private coldStorageType : StorageType
// Cache configuration
private hotCacheMaxSize : number
private hotCacheEvictionThreshold : number
private warmCacheTTL : number
private batchSize : number
// Auto-tuning configuration
private autoTune : boolean
private lastAutoTuneTime : number = 0
private autoTuneInterval : number = 5 * 60 * 1000 // 5 minutes
private storageStatistics : any = null
// Storage adapters for warm and cold caches
private warmStorage : any
private coldStorage : any
// Store options for later reference
private options : {
hotCacheMaxSize? : number
hotCacheEvictionThreshold? : number
warmCacheTTL? : number
batchSize? : number
autoTune? : boolean
warmStorage? : any
coldStorage? : any
readOnly? : boolean
environmentConfig ? : {
node ? : {
hotCacheMaxSize? : number
hotCacheEvictionThreshold? : number
warmCacheTTL? : number
batchSize? : number
} ,
browser ? : {
hotCacheMaxSize? : number
hotCacheEvictionThreshold? : number
warmCacheTTL? : number
batchSize? : number
} ,
worker ? : {
hotCacheMaxSize? : number
hotCacheEvictionThreshold? : number
warmCacheTTL? : number
batchSize? : number
} ,
[ key : string ] : {
hotCacheMaxSize? : number
hotCacheEvictionThreshold? : number
warmCacheTTL? : number
batchSize? : number
} | undefined
}
}
/ * *
* Initialize the cache manager
* @param options Configuration options
* /
constructor ( options : {
hotCacheMaxSize? : number
hotCacheEvictionThreshold? : number
warmCacheTTL? : number
batchSize? : number
autoTune? : boolean
warmStorage? : any
coldStorage? : any
readOnly? : boolean
environmentConfig ? : {
node ? : {
hotCacheMaxSize? : number
hotCacheEvictionThreshold? : number
warmCacheTTL? : number
batchSize? : number
} ,
browser ? : {
hotCacheMaxSize? : number
hotCacheEvictionThreshold? : number
warmCacheTTL? : number
batchSize? : number
} ,
worker ? : {
hotCacheMaxSize? : number
hotCacheEvictionThreshold? : number
warmCacheTTL? : number
batchSize? : number
} ,
[ key : string ] : {
hotCacheMaxSize? : number
hotCacheEvictionThreshold? : number
warmCacheTTL? : number
batchSize? : number
} | undefined
}
} = { } ) {
// Store options for later reference
this . options = options
// Detect environment
this . environment = this . detectEnvironment ( )
// Set storage types based on environment
this . warmStorageType = this . detectWarmStorageType ( )
this . coldStorageType = this . detectColdStorageType ( )
// Initialize storage adapters
this . warmStorage = options . warmStorage || this . initializeWarmStorage ( )
this . coldStorage = options . coldStorage || this . initializeColdStorage ( )
// Set auto-tuning flag
this . autoTune = options . autoTune !== undefined ? options.autoTune : true
// Get environment-specific configuration if available
const envConfig = options . environmentConfig ? . [ Environment [ this . environment ] . toLowerCase ( ) ]
// Set default values or use environment-specific values or global values
this . hotCacheMaxSize = envConfig ? . hotCacheMaxSize || options . hotCacheMaxSize || this . detectOptimalCacheSize ( )
this . hotCacheEvictionThreshold = envConfig ? . hotCacheEvictionThreshold || options . hotCacheEvictionThreshold || 0.8
this . warmCacheTTL = envConfig ? . warmCacheTTL || options . warmCacheTTL || 24 * 60 * 60 * 1000 // 24 hours
this . batchSize = envConfig ? . batchSize || options . batchSize || 10
// If auto-tuning is enabled, perform initial tuning
if ( this . autoTune ) {
this . tuneParameters ( )
}
// Log configuration
if ( process . env . DEBUG ) {
console . log ( 'Cache Manager initialized with configuration:' , {
environment : Environment [ this . environment ] ,
hotCacheMaxSize : this.hotCacheMaxSize ,
hotCacheEvictionThreshold : this.hotCacheEvictionThreshold ,
warmCacheTTL : this.warmCacheTTL ,
batchSize : this.batchSize ,
autoTune : this.autoTune ,
warmStorageType : StorageType [ this . warmStorageType ] ,
coldStorageType : StorageType [ this . coldStorageType ]
} )
}
}
/ * *
* Detect the current environment
* /
private detectEnvironment ( ) : Environment {
if ( typeof window !== 'undefined' && typeof document !== 'undefined' ) {
return Environment . BROWSER
} else if ( typeof self !== 'undefined' && typeof window === 'undefined' ) {
// In a worker environment, self is defined but window is not
return Environment . WORKER
} else {
return Environment . NODE
}
}
/ * *
* Detect the optimal cache size based on available memory and operating mode
*
* Enhanced to better handle large datasets in S3 or other storage :
* - Increases cache size for read - only mode
* - Adjusts based on total dataset size when available
* - Provides more aggressive caching for large datasets
* - Optimizes memory usage based on environment
* /
private detectOptimalCacheSize ( ) : number {
try {
// Default to a conservative value
const defaultSize = 1000
// Get the total dataset size if available
const totalItems = this . storageStatistics ?
( this . storageStatistics . totalNodes || 0 ) + ( this . storageStatistics . totalEdges || 0 ) : 0
// Determine if we're dealing with a large dataset (>100K items)
const isLargeDataset = totalItems > 100000
2025-09-11 16:23:32 -07:00
// Check if we're in read-only mode (from parent Brainy instance)
2025-08-26 12:32:21 -07:00
const isReadOnly = this . options ? . readOnly || false
// In Node.js, use available system memory with enhanced allocation
if ( this . environment === Environment . NODE ) {
try {
// For ES module compatibility, we'll use a fixed default value
// since we can't use dynamic imports in a synchronous function
// Use conservative defaults that don't require OS module
// These values are reasonable for most systems
const estimatedTotalMemory = 8 * 1024 * 1024 * 1024 // Assume 8GB total
const estimatedFreeMemory = 4 * 1024 * 1024 * 1024 // Assume 4GB free
// Estimate average entry size (in bytes)
// This is a conservative estimate for complex objects with vectors
const ESTIMATED_BYTES_PER_ENTRY = 1024 // 1KB per entry
// Base memory percentage - 10% by default
let memoryPercentage = 0.1
// Adjust based on operating mode and dataset size
if ( isReadOnly ) {
// In read-only mode, we can use more memory for caching
memoryPercentage = 0.25 // 25% of free memory
// For large datasets in read-only mode, be even more aggressive
if ( isLargeDataset ) {
memoryPercentage = 0.4 // 40% of free memory
}
} else if ( isLargeDataset ) {
// For large datasets in normal mode, increase slightly
memoryPercentage = 0.15 // 15% of free memory
}
// Calculate optimal size based on adjusted percentage
const optimalSize = Math . max (
Math . floor ( estimatedFreeMemory * memoryPercentage / ESTIMATED_BYTES_PER_ENTRY ) ,
1000
)
// If we know the total dataset size, cap at a reasonable percentage
if ( totalItems > 0 ) {
// In read-only mode, we can cache a larger percentage
const maxPercentage = isReadOnly ? 0.5 : 0.3
const maxItems = Math . ceil ( totalItems * maxPercentage )
// Return the smaller of the two to avoid excessive memory usage
return Math . min ( optimalSize , maxItems )
}
return optimalSize
} catch ( error ) {
console . warn ( 'Failed to detect optimal cache size:' , error )
return defaultSize
}
}
// In browser, use navigator.deviceMemory with enhanced allocation
if ( this . environment === Environment . BROWSER && navigator . deviceMemory ) {
// Base entries per GB
let entriesPerGB = 500
// Adjust based on operating mode and dataset size
if ( isReadOnly ) {
entriesPerGB = 800 // More aggressive caching in read-only mode
if ( isLargeDataset ) {
entriesPerGB = 1000 // Even more aggressive for large datasets
}
} else if ( isLargeDataset ) {
entriesPerGB = 600 // Slightly more aggressive for large datasets
}
// Calculate based on device memory
const browserCacheSize = Math . max ( navigator . deviceMemory * entriesPerGB , 1000 )
// If we know the total dataset size, cap at a reasonable percentage
if ( totalItems > 0 ) {
// In read-only mode, we can cache a larger percentage
const maxPercentage = isReadOnly ? 0.4 : 0.25
const maxItems = Math . ceil ( totalItems * maxPercentage )
// Return the smaller of the two to avoid excessive memory usage
return Math . min ( browserCacheSize , maxItems )
}
return browserCacheSize
}
// For worker environments or when memory detection fails
if ( this . environment === Environment . WORKER ) {
// Workers typically have limited memory, be conservative
return isReadOnly ? 2000 : 1000
}
return defaultSize
} catch ( error ) {
console . warn ( 'Error detecting optimal cache size:' , error )
return 1000 // Conservative default
}
}
/ * *
* Async version of detectOptimalCacheSize that uses dynamic imports
* to access system information in Node . js environments
*
* This method provides more accurate memory detection by using
* the OS module ' s dynamic import in Node . js environments
* /
private async detectOptimalCacheSizeAsync ( ) : Promise < number > {
try {
// Default to a conservative value
const defaultSize = 1000
// Get the total dataset size if available
const totalItems = this . storageStatistics ?
( this . storageStatistics . totalNodes || 0 ) + ( this . storageStatistics . totalEdges || 0 ) : 0
// Determine if we're dealing with a large dataset (>100K items)
const isLargeDataset = totalItems > 100000
2025-09-11 16:23:32 -07:00
// Check if we're in read-only mode (from parent Brainy instance)
2025-08-26 12:32:21 -07:00
const isReadOnly = this . options ? . readOnly || false
// Get memory information based on environment
const memoryInfo = await this . detectAvailableMemory ( )
// If memory detection failed, use the synchronous method
if ( ! memoryInfo ) {
return this . detectOptimalCacheSize ( )
}
// Estimate average entry size (in bytes)
// This is a conservative estimate for complex objects with vectors
const ESTIMATED_BYTES_PER_ENTRY = 1024 // 1KB per entry
// Base memory percentage - 10% by default
let memoryPercentage = 0.1
// Adjust based on operating mode and dataset size
if ( isReadOnly ) {
// In read-only mode, we can use more memory for caching
memoryPercentage = 0.25 // 25% of free memory
// For large datasets in read-only mode, be even more aggressive
if ( isLargeDataset ) {
memoryPercentage = 0.4 // 40% of free memory
}
} else if ( isLargeDataset ) {
// For large datasets in normal mode, increase slightly
memoryPercentage = 0.15 // 15% of free memory
}
// Calculate optimal size based on adjusted percentage
const optimalSize = Math . max (
Math . floor ( memoryInfo . freeMemory * memoryPercentage / ESTIMATED_BYTES_PER_ENTRY ) ,
1000
)
// If we know the total dataset size, cap at a reasonable percentage
if ( totalItems > 0 ) {
// In read-only mode, we can cache a larger percentage
const maxPercentage = isReadOnly ? 0.5 : 0.3
const maxItems = Math . ceil ( totalItems * maxPercentage )
// Return the smaller of the two to avoid excessive memory usage
return Math . min ( optimalSize , maxItems )
}
return optimalSize
} catch ( error ) {
console . warn ( 'Error detecting optimal cache size asynchronously:' , error )
return 1000 // Conservative default
}
}
/ * *
* Detects available memory across different environments
*
* This method uses different techniques to detect memory in :
* - Node.js : Uses the OS module with dynamic import
* - Browser : Uses performance . memory or navigator . deviceMemory
* - Worker : Uses performance . memory if available
*
* @returns An object with totalMemory and freeMemory in bytes , or null if detection fails
* /
private async detectAvailableMemory ( ) : Promise < { totalMemory : number , freeMemory : number } | null > {
try {
// Node.js environment
if ( this . environment === Environment . NODE ) {
try {
// Use dynamic import for OS module
const os = await import ( 'os' )
// Get actual system memory information
const totalMemory = os . totalmem ( )
const freeMemory = os . freemem ( )
return { totalMemory , freeMemory }
} catch ( error ) {
console . warn ( 'Failed to detect memory in Node.js environment:' , error )
}
}
// Browser environment
if ( this . environment === Environment . BROWSER ) {
// Try using performance.memory (Chrome only)
if ( performance && ( performance as any ) . memory ) {
const memoryInfo = ( performance as any ) . memory
// jsHeapSizeLimit is the maximum size of the heap
// totalJSHeapSize is the currently allocated heap size
// usedJSHeapSize is the amount of heap currently being used
const totalMemory = memoryInfo . jsHeapSizeLimit || 0
const usedMemory = memoryInfo . usedJSHeapSize || 0
const freeMemory = Math . max ( totalMemory - usedMemory , 0 )
return { totalMemory , freeMemory }
}
// Try using navigator.deviceMemory as fallback
if ( navigator . deviceMemory ) {
// deviceMemory is in GB, convert to bytes
const totalMemory = navigator . deviceMemory * 1024 * 1024 * 1024
// Assume 50% is free
const freeMemory = totalMemory * 0.5
return { totalMemory , freeMemory }
}
}
// Worker environment
if ( this . environment === Environment . WORKER ) {
// Try using performance.memory if available (Chrome workers)
if ( performance && ( performance as any ) . memory ) {
const memoryInfo = ( performance as any ) . memory
const totalMemory = memoryInfo . jsHeapSizeLimit || 0
const usedMemory = memoryInfo . usedJSHeapSize || 0
const freeMemory = Math . max ( totalMemory - usedMemory , 0 )
return { totalMemory , freeMemory }
}
// For workers, use a conservative estimate
// Assume 2GB total memory with 1GB free
return {
totalMemory : 2 * 1024 * 1024 * 1024 ,
freeMemory : 1 * 1024 * 1024 * 1024
}
}
// If all detection methods fail, use conservative defaults
return {
totalMemory : 8 * 1024 * 1024 * 1024 , // Assume 8GB total
freeMemory : 4 * 1024 * 1024 * 1024 // Assume 4GB free
}
} catch ( error ) {
console . warn ( 'Memory detection failed:' , error )
return null
}
}
/ * *
* Tune cache parameters based on statistics and environment
* This method is called periodically if auto - tuning is enabled
*
* The auto - tuning process :
* 1 . Retrieves storage statistics if available
* 2 . Tunes each parameter based on statistics and environment
* 3 . Logs the tuned parameters if debug is enabled
*
* Auto - tuning helps optimize cache performance by adapting to :
* - The current environment ( Node . js , browser , worker )
* - Available system resources ( memory , CPU )
* - Usage patterns ( read - heavy vs . write - heavy workloads )
* - Cache efficiency ( hit / miss ratios )
* /
private async tuneParameters ( ) : Promise < void > {
// Skip if auto-tuning is disabled
if ( ! this . autoTune ) return
// Check if it's time to tune parameters
const now = Date . now ( )
if ( now - this . lastAutoTuneTime < this . autoTuneInterval ) return
// Update last tune time
this . lastAutoTuneTime = now
try {
// Get storage statistics if available
if ( this . coldStorage && typeof this . coldStorage . getStatistics === 'function' ) {
this . storageStatistics = await this . coldStorage . getStatistics ( )
}
// Get cache statistics for adaptive tuning
const cacheStats = this . getStats ( )
// Use the async version of tuneHotCacheSize which uses detectOptimalCacheSizeAsync
await this . tuneHotCacheSize ( )
// Tune eviction threshold based on hit/miss ratio
this . tuneEvictionThreshold ( cacheStats )
// Tune warm cache TTL based on access patterns
this . tuneWarmCacheTTL ( cacheStats )
// Tune batch size based on access patterns and storage type
this . tuneBatchSize ( cacheStats )
// Log tuned parameters if debug is enabled
if ( process . env . DEBUG ) {
console . log ( 'Cache parameters auto-tuned:' , {
hotCacheMaxSize : this.hotCacheMaxSize ,
hotCacheEvictionThreshold : this.hotCacheEvictionThreshold ,
warmCacheTTL : this.warmCacheTTL ,
batchSize : this.batchSize ,
cacheStats : {
hotCacheSize : cacheStats.hotCacheSize ,
warmCacheSize : cacheStats.warmCacheSize ,
hotCacheHits : cacheStats.hotCacheHits ,
hotCacheMisses : cacheStats.hotCacheMisses ,
warmCacheHits : cacheStats.warmCacheHits ,
warmCacheMisses : cacheStats.warmCacheMisses
}
} )
}
} catch ( error ) {
console . warn ( 'Error during cache parameter auto-tuning:' , error )
}
}
/ * *
* Tune hot cache size based on statistics , environment , and operating mode
*
* The hot cache size is tuned based on :
* 1 . Available memory in the current environment
* 2 . Total number of nodes and edges in the system
* 3 . Cache hit / miss ratio
* 4 . Operating mode ( read - only vs . read - write )
* 5 . Storage type ( S3 , filesystem , memory )
*
* Enhanced algorithm :
* - Start with a size based on available memory and operating mode
* - For large datasets in S3 or other remote storage , use more aggressive caching
* - Adjust based on access patterns ( read - heavy vs . write - heavy )
* - For read - only mode , prioritize cache size over eviction speed
* - Dynamically adjust based on hit / miss ratio and query patterns
* /
private async tuneHotCacheSize ( ) : Promise < void > {
// Use the async version to get more accurate memory information
let optimalSize = await this . detectOptimalCacheSizeAsync ( )
// Check if we're in read-only mode
const isReadOnly = this . options ? . readOnly || false
// Check if we're using S3 or other remote storage
const isRemoteStorage =
this . coldStorageType === StorageType . S3 ||
this . coldStorageType === StorageType . REMOTE_API
// If we have storage statistics, adjust based on total nodes/edges
if ( this . storageStatistics ) {
const totalItems = ( this . storageStatistics . totalNodes || 0 ) +
( this . storageStatistics . totalEdges || 0 )
// If total items is significant, adjust cache size
if ( totalItems > 0 ) {
// Base percentage to cache - adjusted based on mode and storage
let percentageToCache = 0.2 // Cache 20% of items by default
// For read-only mode, increase cache percentage
if ( isReadOnly ) {
percentageToCache = 0.3 // 30% for read-only mode
// For remote storage in read-only mode, be even more aggressive
if ( isRemoteStorage ) {
percentageToCache = 0.4 // 40% for remote storage in read-only mode
}
}
// For remote storage in normal mode, increase slightly
else if ( isRemoteStorage ) {
percentageToCache = 0.25 // 25% for remote storage
}
// For large datasets, cap the percentage to avoid excessive memory usage
if ( totalItems > 1000000 ) { // Over 1 million items
percentageToCache = Math . min ( percentageToCache , 0.15 )
} else if ( totalItems > 100000 ) { // Over 100K items
percentageToCache = Math . min ( percentageToCache , 0.25 )
}
const statisticsBasedSize = Math . ceil ( totalItems * percentageToCache )
// Use the smaller of the two to avoid memory issues
optimalSize = Math . min ( optimalSize , statisticsBasedSize )
}
}
// Adjust based on hit/miss ratio if we have enough data
const totalAccesses = this . stats . hits + this . stats . misses
if ( totalAccesses > 100 ) {
const hitRatio = this . stats . hits / totalAccesses
// Base adjustment factor
let hitRatioFactor = 1.0
// If hit ratio is low, we might need a larger cache
if ( hitRatio < 0.5 ) {
// Calculate adjustment factor based on hit ratio
const baseAdjustment = 0.5 - hitRatio
// For read-only mode or remote storage, be more aggressive
if ( isReadOnly || isRemoteStorage ) {
hitRatioFactor = 1 + ( baseAdjustment * 1.5 ) // Up to 75% increase
} else {
hitRatioFactor = 1 + baseAdjustment // Up to 50% increase
}
optimalSize = Math . ceil ( optimalSize * hitRatioFactor )
}
// If hit ratio is very high, we might be able to reduce cache size slightly
else if ( hitRatio > 0.9 && ! isReadOnly && ! isRemoteStorage ) {
// Only reduce cache size in normal mode with local storage
// and only if hit ratio is very high
hitRatioFactor = 0.9 // 10% reduction
optimalSize = Math . ceil ( optimalSize * hitRatioFactor )
}
}
// Check for operation patterns if available
if ( this . storageStatistics ? . operations ) {
const ops = this . storageStatistics . operations
const totalOps = ops . total || 1
// Calculate read/write ratio
const readOps = ( ops . search || 0 ) + ( ops . get || 0 )
const writeOps = ( ops . add || 0 ) + ( ops . update || 0 ) + ( ops . delete || 0 )
if ( totalOps > 100 ) {
const readRatio = readOps / totalOps
// For read-heavy workloads, increase cache size
if ( readRatio > 0.8 ) {
// More aggressive for remote storage
const readAdjustment = isRemoteStorage ? 1.3 : 1.2
optimalSize = Math . ceil ( optimalSize * readAdjustment )
}
}
}
// Ensure we have a reasonable minimum size based on environment and mode
let minSize = 1000 // Default minimum
// For read-only mode, use a higher minimum
if ( isReadOnly ) {
minSize = 2000
}
// For remote storage, use an even higher minimum
if ( isRemoteStorage ) {
minSize = isReadOnly ? 3000 : 2000
}
optimalSize = Math . max ( optimalSize , minSize )
// Update the hot cache max size
this . hotCacheMaxSize = optimalSize
this . stats . maxSize = optimalSize
}
/ * *
* Tune eviction threshold based on statistics
*
* The eviction threshold determines when items start being evicted from the hot cache .
* It is tuned based on :
* 1 . Cache hit / miss ratio
* 2 . Operation patterns ( read - heavy vs . write - heavy workloads )
* 3 . Memory pressure and available resources
*
* Algorithm :
* - Start with a default threshold of 0.8 ( 80 % of max size )
* - For high hit ratios , increase the threshold to keep more items in cache
* - For low hit ratios , decrease the threshold to evict items more aggressively
* - For read - heavy workloads , use a higher threshold
* - For write - heavy workloads , use a lower threshold
* - Under memory pressure , use a lower threshold to conserve resources
*
* @param cacheStats Optional cache statistics for more adaptive tuning
* /
private tuneEvictionThreshold ( cacheStats? : CacheStats ) : void {
// Default threshold
let threshold = 0.8
// Use provided cache stats or internal stats
const stats = cacheStats || this . getStats ( )
// Adjust based on hit/miss ratio if we have enough data
const totalHotAccesses = stats . hotCacheHits + stats . hotCacheMisses
if ( totalHotAccesses > 100 ) {
const hotHitRatio = stats . hotCacheHits / totalHotAccesses
// If hit ratio is high, we can use a higher threshold
// If hit ratio is low, we should use a lower threshold to evict more aggressively
if ( hotHitRatio > 0.8 ) {
// High hit ratio, increase threshold (up to 0.9)
threshold = Math . min ( 0.9 , 0.8 + ( hotHitRatio - 0.8 ) * 0.5 )
} else if ( hotHitRatio < 0.5 ) {
// Low hit ratio, decrease threshold (down to 0.6)
threshold = Math . max ( 0.6 , 0.8 - ( 0.5 - hotHitRatio ) * 0.5 )
}
}
// If we have storage statistics with operation counts, adjust based on operation patterns
if ( this . storageStatistics && this . storageStatistics . operations ) {
const ops = this . storageStatistics . operations
const totalOps = ops . total || 1
// Calculate read/write ratio
const readOps = ops . search || 0
const writeOps = ( ops . add || 0 ) + ( ops . update || 0 ) + ( ops . delete || 0 )
if ( totalOps > 100 ) {
const readRatio = readOps / totalOps
const writeRatio = writeOps / totalOps
// For read-heavy workloads, use higher threshold
// For write-heavy workloads, use lower threshold
if ( readRatio > 0.8 ) {
// Read-heavy, increase threshold slightly
threshold = Math . min ( 0.9 , threshold + 0.05 )
} else if ( writeRatio > 0.5 ) {
// Write-heavy, decrease threshold
threshold = Math . max ( 0.6 , threshold - 0.1 )
}
}
}
// Check memory pressure - if hot cache is growing too fast relative to hits,
// reduce the threshold to conserve memory
if ( stats . hotCacheSize > 0 && totalHotAccesses > 0 ) {
const sizeToAccessRatio = stats . hotCacheSize / totalHotAccesses
// If the ratio is high, it means we're caching a lot but not getting many hits
if ( sizeToAccessRatio > 10 ) {
// Reduce threshold more aggressively under high memory pressure
threshold = Math . max ( 0.5 , threshold - 0.1 )
}
}
// If we're in read-only mode, we can be more aggressive with caching
const isReadOnly = this . options ? . readOnly || false
if ( isReadOnly ) {
threshold = Math . min ( 0.95 , threshold + 0.05 )
}
// Update the eviction threshold
this . hotCacheEvictionThreshold = threshold
}
/ * *
* Tune warm cache TTL based on statistics
*
* The warm cache TTL determines how long items remain in the warm cache .
* It is tuned based on :
* 1 . Update frequency from operation statistics
* 2 . Warm cache hit / miss ratio
* 3 . Access patterns and frequency
* 4 . Available storage resources
*
* Algorithm :
* - Start with a default TTL of 24 hours
* - For frequently updated data , use a shorter TTL
* - For rarely updated data , use a longer TTL
* - For frequently accessed data , use a longer TTL
* - For rarely accessed data , use a shorter TTL
* - Under storage pressure , use a shorter TTL
*
* @param cacheStats Optional cache statistics for more adaptive tuning
* /
private tuneWarmCacheTTL ( cacheStats? : CacheStats ) : void {
// Default TTL (24 hours)
let ttl = 24 * 60 * 60 * 1000
// Use provided cache stats or internal stats
const stats = cacheStats || this . getStats ( )
// Adjust based on warm cache hit/miss ratio if we have enough data
const totalWarmAccesses = stats . warmCacheHits + stats . warmCacheMisses
if ( totalWarmAccesses > 50 ) {
const warmHitRatio = stats . warmCacheHits / totalWarmAccesses
// If warm cache hit ratio is high, items in warm cache are useful
// so we should keep them longer
if ( warmHitRatio > 0.7 ) {
// High hit ratio, increase TTL (up to 36 hours)
ttl = Math . min ( 36 * 60 * 60 * 1000 , ttl * ( 1 + ( warmHitRatio - 0.7 ) ) )
} else if ( warmHitRatio < 0.3 ) {
// Low hit ratio, decrease TTL (down to 12 hours)
ttl = Math . max ( 12 * 60 * 60 * 1000 , ttl * ( 0.8 - ( 0.3 - warmHitRatio ) ) )
}
}
// If we have storage statistics with operation counts, adjust based on update frequency
if ( this . storageStatistics && this . storageStatistics . operations ) {
const ops = this . storageStatistics . operations
const totalOps = ops . total || 1
const updateOps = ( ops . update || 0 )
if ( totalOps > 100 ) {
const updateRatio = updateOps / totalOps
// For frequently updated data, use shorter TTL
// For rarely updated data, use longer TTL
if ( updateRatio > 0.3 ) {
// Frequently updated, decrease TTL (down to 6 hours)
ttl = Math . max ( 6 * 60 * 60 * 1000 , ttl * ( 1 - updateRatio * 0.5 ) )
} else if ( updateRatio < 0.1 ) {
// Rarely updated, increase TTL (up to 48 hours)
ttl = Math . min ( 48 * 60 * 60 * 1000 , ttl * ( 1.2 - updateRatio ) )
}
}
}
// Check warm cache size relative to hot cache size
// If warm cache is much larger than hot cache, reduce TTL to prevent excessive storage use
if ( stats . warmCacheSize > 0 && stats . hotCacheSize > 0 ) {
const warmToHotRatio = stats . warmCacheSize / stats . hotCacheSize
if ( warmToHotRatio > 5 ) {
// Warm cache is much larger than hot cache, reduce TTL
ttl = Math . max ( 6 * 60 * 60 * 1000 , ttl * ( 0.9 - Math . min ( 0.3 , ( warmToHotRatio - 5 ) / 20 ) ) )
}
}
// If we're in read-only mode, we can use a longer TTL
const isReadOnly = this . options ? . readOnly || false
if ( isReadOnly ) {
ttl = Math . min ( 72 * 60 * 60 * 1000 , ttl * 1.5 )
}
// Update the warm cache TTL
this . warmCacheTTL = ttl
}
/ * *
* Tune batch size based on environment , statistics , and operating mode
*
* The batch size determines how many items are processed in a single batch
* for operations like prefetching . It is tuned based on :
* 1 . Current environment ( Node . js , browser , worker )
* 2 . Available memory
* 3 . Operation patterns
* 4 . Cache hit / miss ratio
* 5 . Operating mode ( read - only vs . read - write )
* 6 . Storage type ( S3 , filesystem , memory )
* 7 . Dataset size
* 8 . Cache efficiency and access patterns
*
* Enhanced algorithm :
* - Start with a default based on the environment
* - For large datasets in S3 or other remote storage , use larger batches
* - For read - only mode , use larger batches to improve throughput
* - Dynamically adjust based on network latency and throughput
* - Balance between memory usage and performance
* - Adapt to cache hit / miss patterns
*
* @param cacheStats Optional cache statistics for more adaptive tuning
* /
private tuneBatchSize ( cacheStats? : CacheStats ) : void {
// Default batch size
let batchSize = 10
// Use provided cache stats or internal stats
const stats = cacheStats || this . getStats ( )
// Check if we're in read-only mode
const isReadOnly = this . options ? . readOnly || false
// Check if we're using S3 or other remote storage
const isRemoteStorage =
this . coldStorageType === StorageType . S3 ||
this . coldStorageType === StorageType . REMOTE_API
// Get the total dataset size if available
const totalItems = this . storageStatistics ?
( this . storageStatistics . totalNodes || 0 ) + ( this . storageStatistics . totalEdges || 0 ) : 0
// Determine if we're dealing with a large dataset
const isLargeDataset = totalItems > 100000
const isVeryLargeDataset = totalItems > 1000000
// Base batch size adjustment based on environment
if ( this . environment === Environment . NODE ) {
// Node.js can handle larger batches
batchSize = isReadOnly ? 30 : 20
// For remote storage, increase batch size
if ( isRemoteStorage ) {
batchSize = isReadOnly ? 50 : 30
}
// For large datasets, adjust batch size
if ( isLargeDataset ) {
batchSize = Math . min ( 100 , batchSize * 1.5 )
}
// For very large datasets, adjust even more
if ( isVeryLargeDataset ) {
batchSize = Math . min ( 200 , batchSize * 2 )
}
} else if ( this . environment === Environment . BROWSER ) {
// Browsers might need smaller batches
batchSize = isReadOnly ? 15 : 10
// If we have memory information, adjust accordingly
if ( navigator . deviceMemory ) {
// Scale batch size with available memory
const memoryFactor = isReadOnly ? 3 : 2
batchSize = Math . max ( 5 , Math . min ( 30 , Math . floor ( navigator . deviceMemory * memoryFactor ) ) )
// For large datasets, adjust based on memory
if ( isLargeDataset && navigator . deviceMemory > 4 ) {
batchSize = Math . min ( 50 , batchSize * 1.5 )
}
}
} else if ( this . environment === Environment . WORKER ) {
// Workers can handle moderate batch sizes
batchSize = isReadOnly ? 20 : 15
}
// Adjust based on cache hit/miss ratios
const totalHotAccesses = stats . hotCacheHits + stats . hotCacheMisses
const totalWarmAccesses = stats . warmCacheHits + stats . warmCacheMisses
if ( totalHotAccesses > 100 ) {
const hotHitRatio = stats . hotCacheHits / totalHotAccesses
// If hot cache hit ratio is high, we're effectively using the cache
// so we can use larger batches for better throughput
if ( hotHitRatio > 0.8 ) {
// High hit ratio, increase batch size
batchSize = Math . min ( batchSize * 1.5 , isRemoteStorage ? 250 : 150 )
} else if ( hotHitRatio < 0.4 ) {
// Low hit ratio, we might be fetching too much at once
// Reduce batch size to be more selective
batchSize = Math . max ( 5 , batchSize * 0.8 )
}
}
if ( totalWarmAccesses > 50 ) {
const warmHitRatio = stats . warmCacheHits / totalWarmAccesses
// If warm cache hit ratio is high, prefetching is effective
// so we can use larger batches
if ( warmHitRatio > 0.7 ) {
// High warm hit ratio, increase batch size
batchSize = Math . min ( batchSize * 1.3 , isRemoteStorage ? 200 : 120 )
} else if ( warmHitRatio < 0.3 ) {
// Low warm hit ratio, reduce batch size
batchSize = Math . max ( 5 , batchSize * 0.9 )
}
}
// If we have storage statistics with operation counts, adjust based on operation patterns
if ( this . storageStatistics && this . storageStatistics . operations ) {
const ops = this . storageStatistics . operations
const totalOps = ops . total || 1
const searchOps = ( ops . search || 0 )
const getOps = ( ops . get || 0 )
if ( totalOps > 100 ) {
// Calculate search and get ratios
const searchRatio = searchOps / totalOps
const getRatio = getOps / totalOps
// For search-heavy workloads, use larger batch size
if ( searchRatio > 0.6 ) {
// Search-heavy, increase batch size
const searchFactor = isRemoteStorage ? 1.8 : 1.5
batchSize = Math . min ( isRemoteStorage ? 200 : 100 , Math . ceil ( batchSize * searchFactor ) )
}
// For get-heavy workloads, adjust batch size
if ( getRatio > 0.6 ) {
// Get-heavy, adjust batch size based on storage type
if ( isRemoteStorage ) {
// For remote storage, larger batches reduce network overhead
batchSize = Math . min ( 150 , Math . ceil ( batchSize * 1.5 ) )
} else {
// For local storage, smaller batches might be more efficient
batchSize = Math . max ( 10 , Math . ceil ( batchSize * 0.9 ) )
}
}
}
}
// Check if we're experiencing memory pressure
if ( stats . hotCacheSize > 0 && this . hotCacheMaxSize > 0 ) {
const cacheUtilization = stats . hotCacheSize / this . hotCacheMaxSize
// If cache utilization is high, reduce batch size to avoid memory pressure
if ( cacheUtilization > 0.85 ) {
batchSize = Math . max ( 5 , Math . floor ( batchSize * 0.8 ) )
}
}
// Adjust based on overall hit/miss ratio if we have enough data
const totalAccesses = stats . hotCacheHits + stats . hotCacheMisses + stats . warmCacheHits + stats . warmCacheMisses
if ( totalAccesses > 100 ) {
const hitRatio = ( stats . hotCacheHits + stats . warmCacheHits ) / totalAccesses
// Base adjustment factors
let increaseFactorForLowHitRatio = isRemoteStorage ? 1.5 : 1.2
let decreaseFactorForHighHitRatio = 0.8
// In read-only mode, be more aggressive with batch size adjustments
if ( isReadOnly ) {
increaseFactorForLowHitRatio = isRemoteStorage ? 2.0 : 1.5
decreaseFactorForHighHitRatio = 0.9 // Less reduction in read-only mode
}
// If hit ratio is high, we can use smaller batches
if ( hitRatio > 0.8 && ! isVeryLargeDataset ) {
// High hit ratio, decrease batch size slightly
// But don't decrease too much for large datasets or remote storage
if ( ! ( isLargeDataset && isRemoteStorage ) ) {
batchSize = Math . max ( isReadOnly ? 10 : 5 , Math . floor ( batchSize * decreaseFactorForHighHitRatio ) )
}
}
// If hit ratio is low, we need larger batches
else if ( hitRatio < 0.5 ) {
// Low hit ratio, increase batch size
const maxBatchSize = isRemoteStorage ?
( isVeryLargeDataset ? 300 : 200 ) :
( isVeryLargeDataset ? 150 : 100 )
batchSize = Math . min ( maxBatchSize , Math . ceil ( batchSize * increaseFactorForLowHitRatio ) )
}
}
// Set minimum batch sizes based on storage type and mode
let minBatchSize = 5
if ( isRemoteStorage ) {
minBatchSize = isReadOnly ? 20 : 10
} else if ( isReadOnly ) {
minBatchSize = 10
}
// Ensure batch size is within reasonable limits
batchSize = Math . max ( minBatchSize , batchSize )
// Cap maximum batch size based on environment and storage
const maxBatchSize = isRemoteStorage ?
( this . environment === Environment . NODE ? 300 : 150 ) :
( this . environment === Environment . NODE ? 150 : 75 )
batchSize = Math . min ( maxBatchSize , batchSize )
// Update the batch size with the adaptively tuned value
this . batchSize = Math . round ( batchSize )
}
/ * *
* Detect the appropriate warm storage type based on environment
* /
private detectWarmStorageType ( ) : StorageType {
if ( this . environment === Environment . BROWSER ) {
// Use OPFS if available, otherwise use memory
if ( 'storage' in navigator && 'getDirectory' in navigator . storage ) {
return StorageType . OPFS
}
return StorageType . MEMORY
} else if ( this . environment === Environment . WORKER ) {
// Use OPFS if available, otherwise use memory
if ( 'storage' in self && 'getDirectory' in ( self as WorkerGlobalScope ) . storage ! ) {
return StorageType . OPFS
}
return StorageType . MEMORY
} else {
// In Node.js, use filesystem
return StorageType . FILESYSTEM
}
}
/ * *
* Detect the appropriate cold storage type based on environment
* /
private detectColdStorageType ( ) : StorageType {
if ( this . environment === Environment . BROWSER ) {
// Use OPFS if available, otherwise use memory
if ( 'storage' in navigator && 'getDirectory' in navigator . storage ) {
return StorageType . OPFS
}
return StorageType . MEMORY
} else if ( this . environment === Environment . WORKER ) {
// Use OPFS if available, otherwise use memory
if ( 'storage' in self && 'getDirectory' in ( self as WorkerGlobalScope ) . storage ! ) {
return StorageType . OPFS
}
return StorageType . MEMORY
} else {
// In Node.js, use S3 if configured, otherwise filesystem
return StorageType . S3
}
}
/ * *
* Initialize warm storage adapter
* /
private initializeWarmStorage ( ) : any {
// Implementation depends on the detected storage type
// For now, return null as this will be provided by the storage adapter
return null
}
/ * *
* Initialize cold storage adapter
* /
private initializeColdStorage ( ) : any {
// Implementation depends on the detected storage type
// For now, return null as this will be provided by the storage adapter
return null
}
/ * *
* Get an item from cache , trying each level in order
* @param id The item ID
* @returns The cached item or null if not found
* /
public async get ( id : string ) : Promise < T | null > {
// Check if it's time to tune parameters
await this . checkAndTuneParameters ( )
// Try hot cache first (fastest)
const hotCacheEntry = this . hotCache . get ( id )
if ( hotCacheEntry ) {
// Update access metadata
hotCacheEntry . lastAccessed = Date . now ( )
hotCacheEntry . accessCount ++
// Update stats
this . stats . hits ++
return hotCacheEntry . data
}
// Try warm cache next
try {
const warmCacheItem = await this . getFromWarmCache ( id )
if ( warmCacheItem ) {
// Promote to hot cache
this . addToHotCache ( id , warmCacheItem )
// Update stats
this . stats . hits ++
return warmCacheItem
}
} catch ( error ) {
console . warn ( ` Error accessing warm cache for ${ id } : ` , error )
}
// Finally, try cold storage
try {
const coldStorageItem = await this . getFromColdStorage ( id )
if ( coldStorageItem ) {
// Promote to hot and warm caches
this . addToHotCache ( id , coldStorageItem )
await this . addToWarmCache ( id , coldStorageItem )
// Update stats
this . stats . misses ++
return coldStorageItem
}
} catch ( error ) {
console . warn ( ` Error accessing cold storage for ${ id } : ` , error )
}
// Item not found in any cache level
this . stats . misses ++
return null
}
/ * *
* Get an item from warm cache
* @param id The item ID
* @returns The cached item or null if not found
* /
private async getFromWarmCache ( id : string ) : Promise < T | null > {
if ( ! this . warmStorage ) return null
try {
return await this . warmStorage . get ( id )
} catch ( error ) {
console . warn ( ` Error getting item ${ id } from warm cache: ` , error )
return null
}
}
/ * *
* Get an item from cold storage
* @param id The item ID
* @returns The item or null if not found
* /
private async getFromColdStorage ( id : string ) : Promise < T | null > {
if ( ! this . coldStorage ) return null
try {
return await this . coldStorage . get ( id )
} catch ( error ) {
console . warn ( ` Error getting item ${ id } from cold storage: ` , error )
return null
}
}
/ * *
* Add an item to hot cache
* @param id The item ID
* @param item The item to cache
* /
private addToHotCache ( id : string , item : T ) : void {
// Check if we need to evict items
if ( this . hotCache . size >= this . hotCacheMaxSize * this . hotCacheEvictionThreshold ) {
this . evictFromHotCache ( )
}
// Add to hot cache
this . hotCache . set ( id , {
data : item ,
lastAccessed : Date.now ( ) ,
accessCount : 1 ,
expiresAt : null // Hot cache items don't expire
} )
// Update stats
this . stats . size = this . hotCache . size
}
/ * *
* Add an item to warm cache
* @param id The item ID
* @param item The item to cache
* /
private async addToWarmCache ( id : string , item : T ) : Promise < void > {
if ( ! this . warmStorage ) return
try {
// Add to warm cache with TTL
await this . warmStorage . set ( id , item , {
ttl : this.warmCacheTTL
} )
} catch ( error ) {
console . warn ( ` Error adding item ${ id } to warm cache: ` , error )
}
}
/ * *
* Evict items from hot cache based on LRU policy
* /
private evictFromHotCache ( ) : void {
// Find the least recently used items
const entries = Array . from ( this . hotCache . entries ( ) )
// Sort by last accessed time (oldest first)
entries . sort ( ( a , b ) = > a [ 1 ] . lastAccessed - b [ 1 ] . lastAccessed )
// Remove the oldest 20% of items
const itemsToRemove = Math . ceil ( this . hotCache . size * 0.2 )
for ( let i = 0 ; i < itemsToRemove && i < entries . length ; i ++ ) {
this . hotCache . delete ( entries [ i ] [ 0 ] )
this . stats . evictions ++
}
// Update stats
this . stats . size = this . hotCache . size
if ( process . env . DEBUG ) {
console . log ( ` Evicted ${ itemsToRemove } items from hot cache, new size: ${ this . hotCache . size } ` )
}
}
/ * *
* Set an item in all cache levels
* @param id The item ID
* @param item The item to cache
* /
public async set ( id : string , item : T ) : Promise < void > {
// Add to hot cache
this . addToHotCache ( id , item )
// Add to warm cache
await this . addToWarmCache ( id , item )
// Add to cold storage
if ( this . coldStorage ) {
try {
await this . coldStorage . set ( id , item )
} catch ( error ) {
console . warn ( ` Error adding item ${ id } to cold storage: ` , error )
}
}
}
/ * *
* Delete an item from all cache levels
* @param id The item ID to delete
* /
public async delete ( id : string ) : Promise < void > {
// Remove from hot cache
this . hotCache . delete ( id )
// Remove from warm cache
if ( this . warmStorage ) {
try {
await this . warmStorage . delete ( id )
} catch ( error ) {
console . warn ( ` Error deleting item ${ id } from warm cache: ` , error )
}
}
// Remove from cold storage
if ( this . coldStorage ) {
try {
await this . coldStorage . delete ( id )
} catch ( error ) {
console . warn ( ` Error deleting item ${ id } from cold storage: ` , error )
}
}
// Update stats
this . stats . size = this . hotCache . size
}
/ * *
* Clear all cache levels
* /
public async clear ( ) : Promise < void > {
// Clear hot cache
this . hotCache . clear ( )
// Clear warm cache
if ( this . warmStorage ) {
try {
await this . warmStorage . clear ( )
} catch ( error ) {
console . warn ( 'Error clearing warm cache:' , error )
}
}
// Clear cold storage
if ( this . coldStorage ) {
try {
await this . coldStorage . clear ( )
} catch ( error ) {
console . warn ( 'Error clearing cold storage:' , error )
}
}
// Reset stats
this . stats = {
hits : 0 ,
misses : 0 ,
evictions : 0 ,
size : 0 ,
maxSize : this.hotCacheMaxSize ,
hotCacheSize : 0 ,
warmCacheSize : 0 ,
hotCacheHits : 0 ,
hotCacheMisses : 0 ,
warmCacheHits : 0 ,
warmCacheMisses : 0
}
}
/ * *
* Get cache statistics
* @returns Cache statistics
* /
public getStats ( ) : CacheStats {
return { . . . this . stats }
}
/ * *
* Prefetch items based on ID patterns or relationships
* @param ids Array of IDs to prefetch
* /
public async prefetch ( ids : string [ ] ) : Promise < void > {
// Check if it's time to tune parameters
await this . checkAndTuneParameters ( )
// Prefetch in batches to avoid overwhelming the system
const batches : string [ ] [ ] = [ ]
// Split into batches using the configurable batch size
for ( let i = 0 ; i < ids . length ; i += this . batchSize ) {
const batch = ids . slice ( i , i + this . batchSize )
batches . push ( batch )
}
// Process each batch
for ( const batch of batches ) {
await Promise . all (
batch . map ( async ( id ) = > {
// Skip if already in hot cache
if ( this . hotCache . has ( id ) ) return
try {
// Try to get from any cache level
await this . get ( id )
} catch ( error ) {
// Ignore errors during prefetching
if ( process . env . DEBUG ) {
console . warn ( ` Error prefetching ${ id } : ` , error )
}
}
} )
)
}
}
/ * *
* Check if it ' s time to tune parameters and do so if needed
* This is called before operations that might benefit from tuned parameters
*
* This method serves as a checkpoint for auto - tuning , ensuring that :
* 1 . Parameters are tuned periodically based on the auto - tune interval
* 2 . Tuning happens before critical operations that would benefit from optimized parameters
* 3 . Tuning doesn ' t happen too frequently , which could impact performance
*
* By calling this method before get ( ) , getMany ( ) , and prefetch ( ) operations ,
* we ensure that the cache parameters are optimized for the current workload
* without adding unnecessary overhead to every operation .
* /
private async checkAndTuneParameters ( ) : Promise < void > {
// Skip if auto-tuning is disabled
if ( ! this . autoTune ) return
// Check if it's time to tune parameters
const now = Date . now ( )
if ( now - this . lastAutoTuneTime >= this . autoTuneInterval ) {
await this . tuneParameters ( )
}
}
/ * *
* Get multiple items at once , optimizing for batch retrieval
* @param ids Array of IDs to get
* @returns Map of ID to item
* /
public async getMany ( ids : string [ ] ) : Promise < Map < string , T > > {
// Check if it's time to tune parameters
await this . checkAndTuneParameters ( )
const result = new Map < string , T > ( )
// First check hot cache for all IDs
const missingIds : string [ ] = [ ]
for ( const id of ids ) {
const hotCacheEntry = this . hotCache . get ( id )
if ( hotCacheEntry ) {
// Update access metadata
hotCacheEntry . lastAccessed = Date . now ( )
hotCacheEntry . accessCount ++
// Add to result
result . set ( id , hotCacheEntry . data )
// Update stats
this . stats . hits ++
} else {
missingIds . push ( id )
}
}
if ( missingIds . length === 0 ) {
return result
}
// Try to get missing items from warm cache
if ( this . warmStorage ) {
try {
const warmCacheItems = await this . warmStorage . getMany ( missingIds )
for ( const [ id , item ] of warmCacheItems . entries ( ) ) {
if ( item ) {
// Promote to hot cache
this . addToHotCache ( id , item )
// Add to result
result . set ( id , item )
// Update stats
this . stats . hits ++
// Remove from missing IDs
const index = missingIds . indexOf ( id )
if ( index !== - 1 ) {
missingIds . splice ( index , 1 )
}
}
}
} catch ( error ) {
console . warn ( 'Error accessing warm cache for batch:' , error )
}
}
if ( missingIds . length === 0 ) {
return result
}
// Try to get remaining missing items from cold storage
if ( this . coldStorage ) {
try {
const coldStorageItems = await this . coldStorage . getMany ( missingIds )
for ( const [ id , item ] of coldStorageItems . entries ( ) ) {
if ( item ) {
// Promote to hot and warm caches
this . addToHotCache ( id , item )
await this . addToWarmCache ( id , item )
// Add to result
result . set ( id , item )
// Update stats
this . stats . misses ++
}
}
} catch ( error ) {
console . warn ( 'Error accessing cold storage for batch:' , error )
}
}
return result
}
/ * *
* Set the storage adapters for warm and cold caches
* @param warmStorage Warm cache storage adapter
* @param coldStorage Cold storage adapter
* /
public setStorageAdapters ( warmStorage : any , coldStorage : any ) : void {
this . warmStorage = warmStorage
this . coldStorage = coldStorage
}
}