import type { IRouteContext } from '../../core/models/route-context.js'; import type { ILogger } from './models/types.js'; /** * Interface for cached function result */ interface ICachedResult { value: T; expiry: number; hash: string; } /** * Function cache for NetworkProxy function-based targets * * This cache improves performance for function-based targets by storing * the results of function evaluations and reusing them for similar contexts. */ export class FunctionCache { // Cache storage private hostCache: Map> = new Map(); private portCache: Map> = new Map(); // Maximum number of entries to store in each cache private maxCacheSize: number; // Default TTL for cache entries in milliseconds (default: 5 seconds) private defaultTtl: number; // Logger private logger: ILogger; /** * Creates a new function cache * * @param logger Logger for debug output * @param options Cache options */ constructor( logger: ILogger, options: { maxCacheSize?: number; defaultTtl?: number; } = {} ) { this.logger = logger; this.maxCacheSize = options.maxCacheSize || 1000; this.defaultTtl = options.defaultTtl || 5000; // 5 seconds default // Start the cache cleanup timer setInterval(() => this.cleanupCache(), 30000); // Cleanup every 30 seconds } /** * Compute a hash for a context object * This is used to identify similar contexts for caching * * @param context The route context to hash * @param functionId Identifier for the function (usually route name or ID) * @returns A string hash */ private computeContextHash(context: IRouteContext, functionId: string): string { // Extract relevant properties for the hash const hashBase = { functionId, port: context.port, domain: context.domain, clientIp: context.clientIp, path: context.path, query: context.query, isTls: context.isTls, tlsVersion: context.tlsVersion }; // Generate a hash string return JSON.stringify(hashBase); } /** * Get cached host result for a function and context * * @param context Route context * @param functionId Identifier for the function * @returns Cached host value or undefined if not found */ public getCachedHost(context: IRouteContext, functionId: string): string | string[] | undefined { const hash = this.computeContextHash(context, functionId); const cached = this.hostCache.get(hash); // Return if no cached value or expired if (!cached || cached.expiry < Date.now()) { if (cached) { // If expired, remove from cache this.hostCache.delete(hash); this.logger.debug(`Cache miss (expired) for host function: ${functionId}`); } else { this.logger.debug(`Cache miss for host function: ${functionId}`); } return undefined; } this.logger.debug(`Cache hit for host function: ${functionId}`); return cached.value; } /** * Get cached port result for a function and context * * @param context Route context * @param functionId Identifier for the function * @returns Cached port value or undefined if not found */ public getCachedPort(context: IRouteContext, functionId: string): number | undefined { const hash = this.computeContextHash(context, functionId); const cached = this.portCache.get(hash); // Return if no cached value or expired if (!cached || cached.expiry < Date.now()) { if (cached) { // If expired, remove from cache this.portCache.delete(hash); this.logger.debug(`Cache miss (expired) for port function: ${functionId}`); } else { this.logger.debug(`Cache miss for port function: ${functionId}`); } return undefined; } this.logger.debug(`Cache hit for port function: ${functionId}`); return cached.value; } /** * Store a host function result in the cache * * @param context Route context * @param functionId Identifier for the function * @param value Host value to cache * @param ttl Optional TTL in milliseconds */ public cacheHost( context: IRouteContext, functionId: string, value: string | string[], ttl?: number ): void { const hash = this.computeContextHash(context, functionId); const expiry = Date.now() + (ttl || this.defaultTtl); // Check if we need to prune the cache before adding if (this.hostCache.size >= this.maxCacheSize) { this.pruneOldestEntries(this.hostCache); } // Store the result this.hostCache.set(hash, { value, expiry, hash }); this.logger.debug(`Cached host function result for: ${functionId}`); } /** * Store a port function result in the cache * * @param context Route context * @param functionId Identifier for the function * @param value Port value to cache * @param ttl Optional TTL in milliseconds */ public cachePort( context: IRouteContext, functionId: string, value: number, ttl?: number ): void { const hash = this.computeContextHash(context, functionId); const expiry = Date.now() + (ttl || this.defaultTtl); // Check if we need to prune the cache before adding if (this.portCache.size >= this.maxCacheSize) { this.pruneOldestEntries(this.portCache); } // Store the result this.portCache.set(hash, { value, expiry, hash }); this.logger.debug(`Cached port function result for: ${functionId}`); } /** * Remove expired entries from the cache */ private cleanupCache(): void { const now = Date.now(); let expiredCount = 0; // Clean up host cache for (const [hash, cached] of this.hostCache.entries()) { if (cached.expiry < now) { this.hostCache.delete(hash); expiredCount++; } } // Clean up port cache for (const [hash, cached] of this.portCache.entries()) { if (cached.expiry < now) { this.portCache.delete(hash); expiredCount++; } } if (expiredCount > 0) { this.logger.debug(`Cleaned up ${expiredCount} expired cache entries`); } } /** * Prune oldest entries from a cache map * Used when the cache exceeds the maximum size * * @param cache The cache map to prune */ private pruneOldestEntries(cache: Map>): void { // Find the oldest entries const now = Date.now(); const itemsToRemove = Math.floor(this.maxCacheSize * 0.2); // Remove 20% of the cache // Convert to array for sorting const entries = Array.from(cache.entries()); // Sort by expiry (oldest first) entries.sort((a, b) => a[1].expiry - b[1].expiry); // Remove oldest entries const toRemove = entries.slice(0, itemsToRemove); for (const [hash] of toRemove) { cache.delete(hash); } this.logger.debug(`Pruned ${toRemove.length} oldest cache entries`); } /** * Get current cache stats */ public getStats(): { hostCacheSize: number; portCacheSize: number } { return { hostCacheSize: this.hostCache.size, portCacheSize: this.portCache.size }; } /** * Clear all cached entries */ public clearCache(): void { this.hostCache.clear(); this.portCache.clear(); this.logger.info('Function cache cleared'); } }