| | |
| | | ...config |
| | | }; |
| | | |
| | | // Check if Cache API is available |
| | | this.cacheAvailable = 'caches' in window; |
| | | |
| | | if(!this.cacheAvailable){ |
| | | console.warn('Browser Cache API unavailable, reverting to LocalStorage'); |
| | | } |
| | | |
| | | // Initialize memory cache |
| | | this._memoryCache = new Map(); |
| | | this._cache = new Map(); |
| | | this.subscribers = new Set(); |
| | | } |
| | | |
| | |
| | | * @returns {number} Number of items cleared |
| | | */ |
| | | clearMemoryCache() { |
| | | const count = this._memoryCache.size; |
| | | this._memoryCache.clear(); |
| | | const count = this._cache.size; |
| | | this._cache.clear(); |
| | | |
| | | console.log(`Cleared ${count} items from memory cache`); |
| | | return count; |
| | |
| | | /** |
| | | * Get a setting value |
| | | */ |
| | | async get(key) { |
| | | get(key) { |
| | | //Check memory cache first |
| | | if (this._cache.has(key)) { |
| | | return this._cache.get(key); |
| | | } |
| | | |
| | | let cacheKey = `${this.base}_${key}`; |
| | | |
| | | const cachedItem = await this.getCacheItem(cacheKey); |
| | | |
| | | if (!cachedItem) { |
| | | 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; |
| | | } |
| | | |
| | | // Deserialize data back to original type |
| | | return this.deserializeData(cachedItem.data, cachedItem.dataType); |
| | | if (item) { |
| | | this._cache.set(key, item); |
| | | } |
| | | return item; |
| | | } |
| | | |
| | | /** |
| | | * Set a setting value |
| | | */ |
| | | async set(key, value) { |
| | | set(key, value) { |
| | | this._cache.set(key, value); |
| | | let cacheKey = `${this.base}_${key}`; |
| | | // Serialize data with type preservation |
| | | const serializedData = this.serializeData(value); |
| | | |
| | | const cacheItem = { |
| | | data: serializedData.data, |
| | | dataType: serializedData.type, |
| | | timestamp: Date.now(), |
| | | }; |
| | | |
| | | await this.setCacheItem(cacheKey, cacheItem); |
| | | |
| | | // Notify subscribers |
| | | this.notify('cache-saved', { key, value }); |
| | | } |
| | | |
| | | remove(key) { |
| | | let cacheKey = `${this.base}_${key}`; |
| | | //TODO: Actually remove it |
| | | } |
| | | |
| | | serializeData(data) { |
| | | // Handle null/undefined |
| | | if (data === null || data === undefined) { |
| | | return { data, type: 'primitive' }; |
| | | } |
| | | |
| | | // Handle Maps |
| | | if (data instanceof Map) { |
| | | return { |
| | | data: Array.from(data.entries()), |
| | | type: 'Map' |
| | | }; |
| | | } |
| | | |
| | | // Handle Sets |
| | | if (data instanceof Set) { |
| | | return { |
| | | data: Array.from(data), |
| | | type: 'Set' |
| | | }; |
| | | } |
| | | |
| | | // Handle Dates |
| | | if (data instanceof Date) { |
| | | return { |
| | | data: data.toISOString(), |
| | | type: 'Date' |
| | | }; |
| | | } |
| | | |
| | | // Handle RegExp |
| | | if (data instanceof RegExp) { |
| | | return { |
| | | data: { source: data.source, flags: data.flags }, |
| | | type: 'RegExp' |
| | | }; |
| | | } |
| | | |
| | | // Handle Arrays (check before general objects) |
| | | if (Array.isArray(data)) { |
| | | // Recursively serialize array elements that might contain Maps/Sets |
| | | return { |
| | | data: data.map(item => this.serializeData(item)), |
| | | type: 'Array' |
| | | }; |
| | | } |
| | | |
| | | // Handle plain objects |
| | | if (data && typeof data === 'object' && data.constructor === Object) { |
| | | const serializedObj = {}; |
| | | for (const [key, value] of Object.entries(data)) { |
| | | serializedObj[key] = this.serializeData(value); |
| | | } |
| | | return { |
| | | data: serializedObj, |
| | | type: 'Object' |
| | | }; |
| | | } |
| | | |
| | | // Handle primitives (string, number, boolean) |
| | | return { data, type: 'primitive' }; |
| | | } |
| | | |
| | | deserializeData(data, type) { |
| | | if (!type || type === 'primitive') { |
| | | return data; |
| | | } |
| | | |
| | | switch (type) { |
| | | case 'Map': |
| | | return new Map(data); |
| | | |
| | | case 'Set': |
| | | return new Set(data); |
| | | |
| | | case 'Date': |
| | | return new Date(data); |
| | | |
| | | case 'RegExp': |
| | | return new RegExp(data.source, data.flags); |
| | | |
| | | case 'Array': |
| | | // Recursively deserialize array elements |
| | | return data.map(item => |
| | | this.deserializeData(item.data, item.type) |
| | | ); |
| | | |
| | | case 'Object': |
| | | const restoredObj = {}; |
| | | for (const [key, value] of Object.entries(data)) { |
| | | restoredObj[key] = this.deserializeData(value.data, value.type); |
| | | } |
| | | return restoredObj; |
| | | |
| | | default: |
| | | console.warn(`Unknown data type: ${type}, returning as-is`); |
| | | return data; |
| | | } |
| | | } |
| | | /********************************************************** |
| | | CACHE |
| | | **********************************************************/ |
| | | /** |
| | | * Main method to get an item from cache |
| | | * Tries Browser Cache API first, falls back to localStorage |
| | | * |
| | | * @param {string} key - Cache key |
| | | * @returns {Promise<any>} - Cached item or null |
| | | */ |
| | | async getCacheItem(key) { |
| | | //Check memory cache first |
| | | if (this._memoryCache.has(key)) { |
| | | return this._memoryCache.get(key); |
| | | } |
| | | |
| | | // Then check persistent cache |
| | | const item = this.cacheAvailable ? |
| | | await this.getBrowserCacheItem(key) : |
| | | this.getLocalStorageItem(key); |
| | | |
| | | // Store in memory cache if found |
| | | if (item) { |
| | | this._memoryCache.set(key, item); |
| | | } |
| | | |
| | | return item; |
| | | } |
| | | |
| | | /** |
| | | * Main method to set an item in cache |
| | | * Uses Browser Cache API if available, falls back to localStorage |
| | | * |
| | | * @param {string} key - Cache key |
| | | * @param {any} item - Item to cache |
| | | * @returns {Promise<void>} |
| | | */ |
| | | async setCacheItem(key, item) { |
| | | // Always store in memory cache |
| | | this._memoryCache.set(key, item); |
| | | |
| | | return this.cacheAvailable ? |
| | | await this.setBrowserCacheItem(key, item) : |
| | | this.setLocalStorageItem(key, item); |
| | | } |
| | | |
| | | /** |
| | | * Remove an item from cache |
| | | * |
| | | * @param {string} key - Cache key |
| | | * @returns {Promise<void>} |
| | | */ |
| | | async removeCacheItem(key) { |
| | | // Remove from memory cache |
| | | this._memoryCache.delete(key); |
| | | |
| | | return this.cacheAvailable ? |
| | | await this.removeBrowserCacheItem(key) : |
| | | this.removeLocalStorageItem(key); |
| | | } |
| | | /********************************************************************* |
| | | BROWSER CACHE |
| | | *********************************************************************/ |
| | | /** |
| | | * Get item from Browser Cache API |
| | | * |
| | | * @param {string} key - Cache key |
| | | * @returns {Promise<any>} - Cached item or null |
| | | */ |
| | | async getBrowserCacheItem(key) { |
| | | try { |
| | | const cache = await caches.open(this.config.namespace); |
| | | const response = await cache.match(key); |
| | | |
| | | if (!response) { |
| | | return null; |
| | | } |
| | | |
| | | return await response.json(); |
| | | } catch (error) { |
| | | console.warn('Error getting from Browser Cache API:', error); |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Set item in Browser Cache API |
| | | * |
| | | * @param {string} key - Cache key |
| | | * @param {any} item - Item to cache |
| | | * @returns {Promise<void>} |
| | | */ |
| | | async setBrowserCacheItem(key, item) { |
| | | try { |
| | | const cache = await caches.open(this.config.namespace); |
| | | const response = new Response(JSON.stringify(item), { |
| | | headers: { 'Content-Type': 'application/json' } |
| | | }); |
| | | |
| | | await cache.put(key, response); |
| | | } catch (error) { |
| | | console.warn('Error setting in Browser Cache API:', error); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Remove item from Browser Cache API |
| | | * |
| | | * @param {string} key - Cache key |
| | | * @returns {Promise<void>} |
| | | */ |
| | | async removeBrowserCacheItem(key) { |
| | | try { |
| | | const cache = await caches.open(this.config.namespace); |
| | | await cache.delete(key); |
| | | } catch (error) { |
| | | console.warn('Error removing from Browser Cache API:', error); |
| | | } |
| | | } |
| | | /************************************************************************* |
| | | LOCAL STORAGE |
| | | *************************************************************************/ |
| | | /** |
| | | * Get item from localStorage |
| | | * |
| | | * @param {string} key - Cache key |
| | | * @returns {object|null} - Cached item or null |
| | | */ |
| | | getLocalStorageItem(key) { |
| | | try { |
| | | const stored = localStorage.getItem(key); |
| | | |
| | | if (!stored) { |
| | | return null; |
| | | } |
| | | |
| | | return JSON.parse(stored); |
| | | } catch (error) { |
| | | console.warn('Error getting from localStorage:', error); |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Set item in localStorage |
| | | * |
| | | * @param {string} key - Cache key |
| | | * @param {any} item - Item to cache |
| | | */ |
| | | setLocalStorageItem(key, item) { |
| | | try { |
| | | localStorage.setItem(key, JSON.stringify(item)); |
| | | localStorage.setItem(cacheKey, JSON.stringify(value)); |
| | | } catch (error) { |
| | | // Handle quota exceeded |
| | | if (error instanceof DOMException && error.code === 22) { |
| | |
| | | console.warn('Error setting localStorage item:', error); |
| | | } |
| | | } |
| | | |
| | | // Notify subscribers |
| | | this.notify('cache-saved', { key, value }); |
| | | } |
| | | |
| | | /** |
| | | * Remove item from localStorage |
| | | * |
| | | * @param {string} key - Cache key |
| | | */ |
| | | removeLocalStorageItem(key) { |
| | | remove(key) { |
| | | let cacheKey = `${this.base}_${key}`; |
| | | try { |
| | | localStorage.removeItem(key); |
| | | localStorage.removeItem(cacheKey); |
| | | } catch (error) { |
| | | console.warn('Error removing localStorage item:', error); |
| | | } |
| | |
| | | console.warn('Error cleaning up localStorage:', error); |
| | | } |
| | | } |
| | | /************************************************************************ |
| | | CLEANUP |
| | | ************************************************************************/ |
| | | /** |
| | | * Clean expired items from cache |
| | | * |
| | | * @returns {Promise<void>} |
| | | */ |
| | | async cleanExpired() { |
| | | const now = Date.now(); |
| | | const maxAge = this.config.TTL; |
| | | |
| | | if (this.cacheAvailable) { |
| | | try { |
| | | const cache = await caches.open(this.options.namespace); |
| | | const keys = await cache.keys(); |
| | | |
| | | for (const request of keys) { |
| | | const response = await cache.match(request); |
| | | |
| | | try { |
| | | const cacheItem = await response.json(); |
| | | if (now - cacheItem.timestamp > maxAge) { |
| | | await cache.delete(request); |
| | | } |
| | | } catch (e) { |
| | | // If we can't parse it, just leave it alone |
| | | } |
| | | } |
| | | } catch (error) { |
| | | console.warn('Error cleaning browser cache:', error); |
| | | } |
| | | } else { |
| | | // Clean localStorage |
| | | try { |
| | | async loadFromCache() { |
| | | for (let i = 0; i < localStorage.length; i++) { |
| | | const key = localStorage.key(i); |
| | | |
| | | if (key && key.startsWith(this.options.namespace)) { |
| | | // Check if key starts with this cache's base prefix |
| | | if (key.startsWith(`${this.base}_`)) { |
| | | let cleanKey = key.replace(`${this.base}_`, ''); |
| | | try { |
| | | const item = JSON.parse(localStorage.getItem(key)); |
| | | |
| | | if (now - item.timestamp > maxAge) { |
| | | localStorage.removeItem(key); |
| | | } |
| | | } catch (e) { |
| | | // Skip invalid items |
| | | } |
| | | } |
| | | } |
| | | // Parse the JSON value before caching |
| | | const value = JSON.parse(localStorage.getItem(key)); |
| | | this._cache.set(cleanKey, value); |
| | | } catch (error) { |
| | | console.warn('Error cleaning localStorage cache:', error); |
| | | console.warn(`Failed to parse cached value for ${key}:`, error); |
| | | } |
| | | } |
| | | |
| | | // Clean memory cache |
| | | for (const [key, item] of this._memoryCache.entries()) { |
| | | if (now - item.timestamp > maxAge) { |
| | | this._memoryCache.delete(key); |
| | | } |
| | | } |
| | | } |
| | | |
| | |
| | | * @returns {Promise<void>} |
| | | */ |
| | | async clear() { |
| | | // Clear memory cache |
| | | this._memoryCache.clear(); |
| | | this._cache.clear(); |
| | | |
| | | if (this.cacheAvailable) { |
| | | try { |
| | | await caches.delete(this.options.namespace); |
| | | } catch (error) { |
| | | console.warn('Error clearing browser cache:', error); |
| | | } |
| | | } |
| | | |
| | | // Also clear localStorage in case we've used it as fallback |
| | | this.clearLocalStorage(); |
| | | } |
| | | |
| | | /** |
| | | * Clear just localStorage cache |
| | | */ |
| | | clearLocalStorage() { |
| | | try { |
| | | for (let i = localStorage.length - 1; i >= 0; i--) { |
| | | const key = localStorage.key(i); |
| | | if (key && key.startsWith(this.options.namespace)) { |
| | | if (key && key.startsWith(this.config.namespace)) { |
| | | localStorage.removeItem(key); |
| | | } |
| | | } |
| | |
| | | console.warn('Error clearing localStorage cache:', error); |
| | | } |
| | | } |
| | | /************************** OLD **********************************/ |
| | | |
| | | /** |
| | | * Subscribe to setting changes |