smartproxy/ts/proxies/network-proxy/function-cache.ts

259 lines
7.4 KiB
TypeScript

import type { IRouteContext } from '../../core/models/route-context.js';
import type { ILogger } from './models/types.js';
/**
* Interface for cached function result
*/
interface ICachedResult<T> {
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<string, ICachedResult<string | string[]>> = new Map();
private portCache: Map<string, ICachedResult<number>> = 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<T>(cache: Map<string, ICachedResult<T>>): 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');
}
}