Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol. wisp.place
1// In-memory LRU cache for file contents and metadata 2 3interface CacheEntry<T> { 4 value: T; 5 size: number; 6 timestamp: number; 7} 8 9interface CacheStats { 10 hits: number; 11 misses: number; 12 evictions: number; 13 currentSize: number; 14 currentCount: number; 15} 16 17export class LRUCache<T> { 18 private cache: Map<string, CacheEntry<T>>; 19 private maxSize: number; 20 private maxCount: number; 21 private currentSize: number; 22 private stats: CacheStats; 23 24 constructor(maxSize: number, maxCount: number) { 25 this.cache = new Map(); 26 this.maxSize = maxSize; 27 this.maxCount = maxCount; 28 this.currentSize = 0; 29 this.stats = { 30 hits: 0, 31 misses: 0, 32 evictions: 0, 33 currentSize: 0, 34 currentCount: 0, 35 }; 36 } 37 38 get(key: string): T | null { 39 const entry = this.cache.get(key); 40 if (!entry) { 41 this.stats.misses++; 42 return null; 43 } 44 45 // Move to end (most recently used) 46 this.cache.delete(key); 47 this.cache.set(key, entry); 48 49 this.stats.hits++; 50 return entry.value; 51 } 52 53 set(key: string, value: T, size: number): void { 54 // Remove existing entry if present 55 if (this.cache.has(key)) { 56 const existing = this.cache.get(key)!; 57 this.currentSize -= existing.size; 58 this.cache.delete(key); 59 } 60 61 // Evict entries if needed 62 while ( 63 (this.cache.size >= this.maxCount || this.currentSize + size > this.maxSize) && 64 this.cache.size > 0 65 ) { 66 const firstKey = this.cache.keys().next().value; 67 if (!firstKey) break; // Should never happen, but satisfy TypeScript 68 const firstEntry = this.cache.get(firstKey); 69 if (!firstEntry) break; // Should never happen, but satisfy TypeScript 70 this.cache.delete(firstKey); 71 this.currentSize -= firstEntry.size; 72 this.stats.evictions++; 73 } 74 75 // Add new entry 76 this.cache.set(key, { 77 value, 78 size, 79 timestamp: Date.now(), 80 }); 81 this.currentSize += size; 82 83 // Update stats 84 this.stats.currentSize = this.currentSize; 85 this.stats.currentCount = this.cache.size; 86 } 87 88 delete(key: string): boolean { 89 const entry = this.cache.get(key); 90 if (!entry) return false; 91 92 this.cache.delete(key); 93 this.currentSize -= entry.size; 94 this.stats.currentSize = this.currentSize; 95 this.stats.currentCount = this.cache.size; 96 return true; 97 } 98 99 // Invalidate all entries for a specific site 100 invalidateSite(did: string, rkey: string): number { 101 const prefix = `${did}:${rkey}:`; 102 let count = 0; 103 104 for (const key of Array.from(this.cache.keys())) { 105 if (key.startsWith(prefix)) { 106 this.delete(key); 107 count++; 108 } 109 } 110 111 return count; 112 } 113 114 // Get cache size 115 size(): number { 116 return this.cache.size; 117 } 118 119 clear(): void { 120 this.cache.clear(); 121 this.currentSize = 0; 122 this.stats.currentSize = 0; 123 this.stats.currentCount = 0; 124 } 125 126 getStats(): CacheStats { 127 return { ...this.stats }; 128 } 129 130 // Get cache hit rate 131 getHitRate(): number { 132 const total = this.stats.hits + this.stats.misses; 133 return total === 0 ? 0 : (this.stats.hits / total) * 100; 134 } 135} 136 137// File metadata cache entry 138export interface FileMetadata { 139 encoding?: 'gzip'; 140 mimeType: string; 141} 142 143// Global cache instances 144const FILE_CACHE_SIZE = 100 * 1024 * 1024; // 100MB 145const FILE_CACHE_COUNT = 500; 146const METADATA_CACHE_COUNT = 2000; 147 148export const fileCache = new LRUCache<Buffer>(FILE_CACHE_SIZE, FILE_CACHE_COUNT); 149export const metadataCache = new LRUCache<FileMetadata>(1024 * 1024, METADATA_CACHE_COUNT); // 1MB for metadata 150export const rewrittenHtmlCache = new LRUCache<Buffer>(50 * 1024 * 1024, 200); // 50MB for rewritten HTML 151 152// Helper to generate cache keys 153export function getCacheKey(did: string, rkey: string, filePath: string, suffix?: string): string { 154 const base = `${did}:${rkey}:${filePath}`; 155 return suffix ? `${base}:${suffix}` : base; 156} 157 158// Invalidate all caches for a site 159export function invalidateSiteCache(did: string, rkey: string): void { 160 const fileCount = fileCache.invalidateSite(did, rkey); 161 const metaCount = metadataCache.invalidateSite(did, rkey); 162 const htmlCount = rewrittenHtmlCache.invalidateSite(did, rkey); 163 164 console.log(`[Cache] Invalidated site ${did}:${rkey} - ${fileCount} files, ${metaCount} metadata, ${htmlCount} HTML`); 165} 166 167// Get overall cache statistics 168export function getCacheStats() { 169 return { 170 files: fileCache.getStats(), 171 fileHitRate: fileCache.getHitRate(), 172 metadata: metadataCache.getStats(), 173 metadataHitRate: metadataCache.getHitRate(), 174 rewrittenHtml: rewrittenHtmlCache.getStats(), 175 rewrittenHtmlHitRate: rewrittenHtmlCache.getHitRate(), 176 }; 177}