/*** * A simpler cache, using localStorage * Mainly used to store settings locally * example: * -> dark/light mode switch * -> view mode selection * -> tab navigation direction (for table views) **/ class SimpleCache { /** * * @param {string} base * @param {object} config * @param {string} config.namespace * @param {number} config.TTL * @param {number} config.maxSize */ constructor(base, config = {}) { this.base = base; this.config = { namespace: 'jvb_cache', TTL: 3600000, maxSize: 100, ...config }; // Initialize memory cache this._cache = new Map(); this.subscribers = new Set(); } /** * Clear all memory cache * @returns {number} Number of items cleared */ clearMemoryCache() { const count = this._cache.size; this._cache.clear(); console.log(`Cleared ${count} items from memory cache`); return count; } /** * Get a setting value */ get(key) { //Check memory cache first if (this._cache.has(key)) { return this._cache.get(key); } let cacheKey = `${this.base}_${key}`; let item; try { item = localStorage.getItem(cacheKey); if (!item) { return null; } item = JSON.parse(item); } catch (error) { console.warn('Error getting from localStorage:', error); return null; } if (item) { this._cache.set(key, item); } return item; } /** * Set a setting value */ set(key, value) { this._cache.set(key, value); let cacheKey = `${this.base}_${key}`; try { localStorage.setItem(cacheKey, JSON.stringify(value)); } catch (error) { // Handle quota exceeded if (error instanceof DOMException && error.code === 22) { this.clearOldestLocalStorageItems(); try { localStorage.setItem(key, JSON.stringify(item)); } catch (retryError) { console.warn('Still failed to set localStorage item after cleanup:', retryError); } } else { console.warn('Error setting localStorage item:', error); } } // Notify subscribers this.notify('cache-saved', { key, value }); } remove(key) { let cacheKey = `${this.base}_${key}`; try { localStorage.removeItem(cacheKey); } catch (error) { console.warn('Error removing localStorage item:', error); } } /** * Clear oldest items from localStorage when quota is exceeded */ clearOldestLocalStorageItems() { try { const keysToRemove = []; // Find all our cache keys for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key.startsWith(this.config.namespace)) { try { const item = JSON.parse(localStorage.getItem(key)); keysToRemove.push({ key, timestamp: item.timestamp || 0 }); } catch (e) { // If it's not valid JSON or doesn't have a timestamp, prioritize for removal keysToRemove.push({ key, timestamp: 0 }); } } } // Sort by timestamp (oldest first) keysToRemove.sort((a, b) => a.timestamp - b.timestamp); // Remove the oldest 20% of items const removeCount = Math.max(1, Math.ceil(keysToRemove.length * 0.2)); for (let i = 0; i < removeCount; i++) { if (keysToRemove[i]) { localStorage.removeItem(keysToRemove[i].key); } } } catch (error) { console.warn('Error cleaning up localStorage:', error); } } async loadFromCache() { for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); // Check if key starts with this cache's base prefix if (key.startsWith(`${this.base}_`)) { let cleanKey = key.replace(`${this.base}_`, ''); try { // Parse the JSON value before caching const value = JSON.parse(localStorage.getItem(key)); this._cache.set(cleanKey, value); } catch (error) { console.warn(`Failed to parse cached value for ${key}:`, error); } } } } /** * Clear all cache * * @returns {Promise} */ async clear() { this._cache.clear(); try { for (let i = localStorage.length - 1; i >= 0; i--) { const key = localStorage.key(i); if (key && key.startsWith(this.config.namespace)) { localStorage.removeItem(key); } } } catch (error) { console.warn('Error clearing localStorage cache:', error); } } /** * Subscribe to setting changes */ subscribe(callback) { this.subscribers.add(callback); return () => this.subscribers.delete(callback); } /** * Notify subscribers */ notify(event, data) { this.subscribers.forEach(cb => cb(event, data)); } } window.jvbCache = SimpleCache;