storage-cache.ts
1 import { safeJsonParseObject } from './json-utils' 2 import { hmrSingleton } from '@/lib/shared-utils' 3 import type { AppSettings } from '@/types' 4 5 // --- TTL Cache (read-through with write-through invalidation) --- 6 7 interface TTLEntry<T> { 8 value: T 9 expiresAt: number 10 } 11 12 /** 13 * Simple TTL cache for hot-path reads that rarely change. 14 * Stored on globalThis so HMR doesn't reset it. 15 */ 16 export class TTLCache<T> { 17 private entry: TTLEntry<T> | null = null 18 constructor(private readonly ttlMs: number) {} 19 20 get(): T | undefined { 21 if (!this.entry) return undefined 22 if (Date.now() > this.entry.expiresAt) { 23 this.entry = null 24 return undefined 25 } 26 return this.entry.value 27 } 28 29 set(value: T): void { 30 this.entry = { value, expiresAt: Date.now() + this.ttlMs } 31 } 32 33 invalidate(): void { 34 this.entry = null 35 } 36 } 37 38 type TTLCacheStore = { 39 settings?: TTLCache<AppSettings> 40 agents?: TTLCache<Record<string, unknown>> 41 sessions?: TTLCache<Record<string, unknown>> 42 } 43 const ttlCaches: TTLCacheStore = hmrSingleton<TTLCacheStore>('__swarmclaw_ttl_caches__', () => ({})) 44 45 export function getSettingsCache() { return ttlCaches.settings ?? (ttlCaches.settings = new TTLCache(60_000)) } 46 export function getAgentsCache() { return ttlCaches.agents ?? (ttlCaches.agents = new TTLCache(15_000)) } 47 export function getSessionsCache() { return ttlCaches.sessions ?? (ttlCaches.sessions = new TTLCache(5_000)) } 48 49 // --- LRU Cache --- 50 51 const DEFAULT_LRU_CAPACITY = 5000 52 53 /** Per-collection capacity overrides from COLLECTION_CACHE_LIMITS env var (JSON). */ 54 export function parseCacheLimits(): Record<string, number> { 55 const raw = process.env.COLLECTION_CACHE_LIMITS 56 if (!raw) return {} 57 const parsed = safeJsonParseObject(raw) 58 if (!parsed) return {} 59 const result: Record<string, number> = {} 60 for (const [k, v] of Object.entries(parsed)) { 61 if (typeof v === 'number' && v > 0) result[k] = v 62 } 63 return result 64 } 65 66 const cacheLimits = parseCacheLimits() 67 68 export function capacityFor(collection: string): number { 69 return cacheLimits[collection] ?? DEFAULT_LRU_CAPACITY 70 } 71 72 /** 73 * A Map wrapper with LRU eviction. JS Maps iterate in insertion order, 74 * so the *first* key is the least-recently-used entry. 75 */ 76 export class LRUMap<K, V> { 77 private readonly map = new Map<K, V>() 78 readonly capacity: number 79 80 constructor(capacity: number) { 81 this.capacity = Math.max(1, capacity) 82 } 83 84 get(key: K): V | undefined { 85 if (!this.map.has(key)) return undefined 86 const value = this.map.get(key)! 87 // Move to end (most-recently-used) 88 this.map.delete(key) 89 this.map.set(key, value) 90 return value 91 } 92 93 set(key: K, value: V): this { 94 if (this.map.has(key)) { 95 this.map.delete(key) 96 } 97 this.map.set(key, value) 98 // Evict oldest if over capacity 99 if (this.map.size > this.capacity) { 100 const oldest = this.map.keys().next().value as K 101 this.map.delete(oldest) 102 } 103 return this 104 } 105 106 has(key: K): boolean { 107 return this.map.has(key) 108 } 109 110 delete(key: K): boolean { 111 return this.map.delete(key) 112 } 113 114 get size(): number { 115 return this.map.size 116 } 117 118 clear(): void { 119 this.map.clear() 120 } 121 122 keys(): MapIterator<K> { 123 return this.map.keys() 124 } 125 126 values(): MapIterator<V> { 127 return this.map.values() 128 } 129 130 entries(): MapIterator<[K, V]> { 131 return this.map.entries() 132 } 133 134 [Symbol.iterator](): MapIterator<[K, V]> { 135 return this.map[Symbol.iterator]() 136 } 137 } 138 139 /** Per-collection LRU cache of raw JSON strings, keyed by record id. */ 140 export const collectionCache: Map<string, LRUMap<string, string>> = 141 hmrSingleton('__swarmclaw_storage_collection_cache__', () => new Map<string, LRUMap<string, string>>()) 142 143 /** TTL caches created by createCollectionStore (factory caches). */ 144 export const factoryTtlCaches = hmrSingleton('__swarmclaw_factory_ttl__', () => new Map<string, TTLCache<Record<string, unknown>>>())