NeahStable/lib/cache-utils.ts
2026-01-16 12:41:28 +01:00

247 lines
6.5 KiB
TypeScript

/**
* Cache utilities for Pages application
* Provides centralized cache management with proper invalidation
*/
interface CacheEntry<T> {
data: T;
timestamp: number;
}
interface CacheConfig {
ttl: number; // Time to live in milliseconds
keyPrefix: string;
}
class CacheManager {
private memoryCache: Map<string, CacheEntry<any>> = new Map();
private config: CacheConfig;
constructor(config: CacheConfig) {
this.config = config;
}
/**
* Get data from cache (memory first, then localStorage)
*/
get<T>(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<T> = 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<T>(key: string, data: T): void {
const fullKey = `${this.config.keyPrefix}${key}`;
const entry: CacheEntry<T> = {
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<any> = 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}`);
}