247 lines
6.5 KiB
TypeScript
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}`);
|
|
}
|