/** * Cache system similar to Python's CacheDict from Tryton * Implements LRU (Least Recently Used) cache using JavaScript Map * TypeScript version */ export type CacheFactory = () => T; export class CacheDict extends Map { private cacheLen: number; private defaultFactory: CacheFactory | null; /** * Create a new CacheDict */ constructor( cacheLen: number = 10, defaultFactory: CacheFactory | null = null ) { super(); this.cacheLen = cacheLen; this.defaultFactory = defaultFactory; } /** * Set a key-value pair and maintain LRU order */ override set(key: K, value: V): this { // If key exists, delete it first to move to end if (this.has(key)) { this.delete(key); } super.set(key, value); // Remove oldest entries if cache is full while (this.size > this.cacheLen) { const firstKey = this.keys().next().value; if (firstKey !== undefined) { this.delete(firstKey); } } return this; } /** * Get a value and move it to end (most recently used) */ override get(key: K): V | undefined { if (this.has(key)) { const value = super.get(key)!; // Move to end by re-setting this.delete(key); super.set(key, value); return value; } // Handle missing key with default factory if (this.defaultFactory) { const value = this.defaultFactory(); this.set(key, value); return value; } return undefined; } /** * Override has() to update LRU order on access */ override has(key: K): boolean { const exists = super.has(key); if (exists) { // Move to end on access const value = super.get(key)!; this.delete(key); super.set(key, value); } return exists; } /** * Get current cache size */ get length(): number { return this.size; } /** * Clear all items from cache */ override clear(): void { super.clear(); } /** * Convert cache to array for debugging */ toArray(): Array<[K, V]> { return Array.from(this.entries()); } } export interface CacheEntry { expire: Date; value: T; } export interface CacheStats { totalPrefixes: number; totalEntries: number; prefixes: Record; } /** * Advanced cache for Tryton RPC with expiration support */ export class TrytonCache { private store: CacheDict>; private cacheLen: number; constructor(cacheLen: number = 1024) { this.cacheLen = cacheLen; this.store = new CacheDict>( cacheLen, () => new CacheDict(cacheLen) ); } /** * Check if a prefix is cached */ cached(prefix: string): boolean { return this.store.has(prefix); } /** * Set cache entry with expiration */ set(prefix: string, key: string, expire: number | Date, value: any): void { let expiration: Date; if (typeof expire === "number") { // Assume seconds, convert to Date expiration = new Date(Date.now() + expire * 1000); } else if (expire instanceof Date) { expiration = expire; } else { throw new Error("Invalid expiration type"); } // Deep copy value to avoid mutations const cachedValue = this._deepCopy(value); this.store.get(prefix)!.set(key, { expire: expiration, value: cachedValue, }); } /** * Get cached value if not expired */ get(prefix: string, key: string): any { const now = new Date(); if (!this.store.has(prefix)) { throw new Error("Key not found"); } const prefixCache = this.store.get(prefix)!; if (!prefixCache.has(key)) { throw new Error("Key not found"); } const entry = prefixCache.get(key)!; if (entry.expire < now) { prefixCache.delete(key); throw new Error("Key expired"); } console.log(`(cached) ${prefix} ${key}`); return this._deepCopy(entry.value); } /** * Clear cache for a specific prefix or all */ clear(prefix?: string): void { if (prefix) { if (this.store.has(prefix)) { this.store.get(prefix)!.clear(); } } else { this.store.clear(); } } /** * Deep copy objects to prevent mutations */ private _deepCopy(obj: T): T { if (obj === null || typeof obj !== "object") { return obj; } if (obj instanceof Date) { return new Date(obj.getTime()) as T; } if (obj instanceof Array) { return obj.map((item) => this._deepCopy(item)) as T; } // Handle Buffer in Node.js environment if (typeof Buffer !== "undefined" && obj instanceof Buffer) { return Buffer.from(obj) as T; } if (typeof obj === "object") { const copy: any = {}; for (const key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { copy[key] = this._deepCopy((obj as any)[key]); } } return copy as T; } return obj; } /** * Get cache statistics */ getStats(): CacheStats { const stats: CacheStats = { totalPrefixes: this.store.size, totalEntries: 0, prefixes: {}, }; for (const [prefix, prefixCache] of this.store.entries()) { const count = prefixCache.size; stats.totalEntries += count; stats.prefixes[prefix] = count; } return stats; } /** * Remove expired entries */ cleanupExpired(): number { const now = new Date(); let removedCount = 0; for (const [prefix, prefixCache] of this.store.entries()) { const keysToRemove: string[] = []; for (const [key, entry] of prefixCache.entries()) { if (entry.expire < now) { keysToRemove.push(key); } } keysToRemove.forEach((key) => { prefixCache.delete(key); removedCount++; }); } return removedCount; } /** * Get cache size in bytes (approximate) */ getSizeEstimate(): number { let size = 0; for (const [prefix, prefixCache] of this.store.entries()) { size += prefix.length * 2; // UTF-16 encoding for (const [key, entry] of prefixCache.entries()) { size += key.length * 2; size += JSON.stringify(entry.value).length * 2; size += 24; // Date object overhead } } return size; } }