/** * Cache utilities for Pages application * Provides centralized cache management with proper invalidation */ interface CacheEntry { data: T; timestamp: number; } interface CacheConfig { ttl: number; // Time to live in milliseconds keyPrefix: string; } class CacheManager { private memoryCache: Map> = new Map(); private config: CacheConfig; constructor(config: CacheConfig) { this.config = config; } /** * Get data from cache (memory first, then localStorage) */ get(key: string): T | null { const fullKey = `${this.config.keyPrefix}${key}`; // Check memory cache first const memoryEntry = this.memoryCache.get(fullKey); if (memoryEntry && this.isValid(memoryEntry.timestamp)) { return memoryEntry.data as T; } // Check localStorage try { const stored = localStorage.getItem(fullKey); if (stored) { const entry: CacheEntry = JSON.parse(stored); if (this.isValid(entry.timestamp)) { // Update memory cache this.memoryCache.set(fullKey, entry); return entry.data; } else { // Expired, remove it localStorage.removeItem(fullKey); } } } catch (error) { console.error('Error reading from localStorage cache:', error); } return null; } /** * Set data in cache (both memory and localStorage) */ set(key: string, data: T): void { const fullKey = `${this.config.keyPrefix}${key}`; const entry: CacheEntry = { data, timestamp: Date.now() }; // Update memory cache this.memoryCache.set(fullKey, entry); // Update localStorage try { localStorage.setItem(fullKey, JSON.stringify(entry)); } catch (error) { console.error('Error writing to localStorage cache:', error); // If localStorage is full, try to clear old entries this.clearExpired(); } } /** * Remove specific cache entry */ remove(key: string): void { const fullKey = `${this.config.keyPrefix}${key}`; // Remove from memory this.memoryCache.delete(fullKey); // Remove from localStorage try { localStorage.removeItem(fullKey); } catch (error) { console.error('Error removing from localStorage cache:', error); } } /** * Clear all cache entries matching a pattern */ clearPattern(pattern: string): void { const fullPattern = `${this.config.keyPrefix}${pattern}`; // Clear from memory for (const key of this.memoryCache.keys()) { if (key.includes(fullPattern)) { this.memoryCache.delete(key); } } // Clear from localStorage try { const keysToRemove: string[] = []; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key && key.includes(fullPattern)) { keysToRemove.push(key); } } keysToRemove.forEach(key => localStorage.removeItem(key)); } catch (error) { console.error('Error clearing pattern from localStorage cache:', error); } } /** * Clear all expired entries */ clearExpired(): void { const now = Date.now(); // Clear from memory for (const [key, entry] of this.memoryCache.entries()) { if (!this.isValid(entry.timestamp)) { this.memoryCache.delete(key); } } // Clear from localStorage try { const keysToRemove: string[] = []; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key && key.startsWith(this.config.keyPrefix)) { try { const entry: CacheEntry = JSON.parse(localStorage.getItem(key) || '{}'); if (!this.isValid(entry.timestamp)) { keysToRemove.push(key); } } catch { // Invalid entry, remove it keysToRemove.push(key); } } } keysToRemove.forEach(key => localStorage.removeItem(key)); } catch (error) { console.error('Error clearing expired entries from localStorage:', error); } } /** * Clear all cache */ clearAll(): void { this.memoryCache.clear(); try { const keysToRemove: string[] = []; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key && key.startsWith(this.config.keyPrefix)) { keysToRemove.push(key); } } keysToRemove.forEach(key => localStorage.removeItem(key)); } catch (error) { console.error('Error clearing all from localStorage cache:', error); } } /** * Check if cache entry is still valid */ private isValid(timestamp: number): boolean { return (Date.now() - timestamp) < this.config.ttl; } } // Export cache managers for different use cases export const notesCache = new CacheManager({ ttl: 2 * 60 * 1000, // 2 minutes (reduced from 5 to ensure fresh data) keyPrefix: 'notes-cache-' }); export const noteContentCache = new CacheManager({ ttl: 15 * 60 * 1000, // 15 minutes keyPrefix: 'note-content-' }); export const foldersCache = new CacheManager({ ttl: 2 * 60 * 1000, // 2 minutes keyPrefix: 'nextcloud_folders' }); /** * Invalidate cache for a specific folder */ export function invalidateFolderCache(userId: string, folder: string): void { const folderLowercase = folder.toLowerCase(); const cacheKey = `${userId}-${folderLowercase}`; // Clear notes list cache const hadCache = notesCache.get(cacheKey) !== null; notesCache.remove(cacheKey); // Clear all note content caches for this folder (pattern match) noteContentCache.clearPattern(`user-${userId}/${folderLowercase}/`); console.log(`[invalidateFolderCache] Cache invalidated for folder: ${folderLowercase} (had cache: ${hadCache})`); // Double-check that cache is cleared const stillCached = notesCache.get(cacheKey); if (stillCached) { console.warn(`[invalidateFolderCache] WARNING: Cache still exists after removal for ${cacheKey}`); // Force clear all matching keys notesCache.clearPattern(cacheKey); } } /** * Invalidate cache for a specific note */ export function invalidateNoteCache(noteId: string): void { noteContentCache.remove(noteId); console.log(`Cache invalidated for note: ${noteId}`); } /** * Clear all caches for a user */ export function clearUserCache(userId: string): void { notesCache.clearPattern(userId); noteContentCache.clearPattern(`user-${userId}/`); foldersCache.clearAll(); console.log(`All caches cleared for user: ${userId}`); }