// modules/api-manager/StorageManager.js import { Logger } from '../../core/logger.js'; /** * Storage APIs Manager - IndexedDB, Cache API, Web Locks, Broadcast Channel */ export class StorageManager { constructor(config = {}) { this.config = config; this.dbName = config.dbName || 'AppDatabase'; this.dbVersion = config.dbVersion || 1; this.db = null; this.cache = null; this.channels = new Map(); // Check API support this.support = { indexedDB: 'indexedDB' in window, cacheAPI: 'caches' in window, webLocks: 'locks' in navigator, broadcastChannel: 'BroadcastChannel' in window, localStorage: 'localStorage' in window, sessionStorage: 'sessionStorage' in window }; Logger.info('[StorageManager] Initialized with support:', this.support); // Initialize databases this.initializeDB(); this.initializeCache(); } /** * Initialize IndexedDB */ async initializeDB() { if (!this.support.indexedDB) { Logger.warn('[StorageManager] IndexedDB not supported'); return; } try { const request = indexedDB.open(this.dbName, this.dbVersion); request.onerror = () => { Logger.error('[StorageManager] IndexedDB failed to open'); }; request.onupgradeneeded = (event) => { const db = event.target.result; // Create default object stores if (!db.objectStoreNames.contains('keyValue')) { const store = db.createObjectStore('keyValue', { keyPath: 'key' }); store.createIndex('timestamp', 'timestamp', { unique: false }); } if (!db.objectStoreNames.contains('cache')) { db.createObjectStore('cache', { keyPath: 'key' }); } if (!db.objectStoreNames.contains('files')) { const fileStore = db.createObjectStore('files', { keyPath: 'id', autoIncrement: true }); fileStore.createIndex('name', 'name', { unique: false }); fileStore.createIndex('type', 'type', { unique: false }); } Logger.info('[StorageManager] IndexedDB schema updated'); }; request.onsuccess = (event) => { this.db = event.target.result; Logger.info('[StorageManager] IndexedDB connected'); }; } catch (error) { Logger.error('[StorageManager] IndexedDB initialization failed:', error); } } /** * Initialize Cache API */ async initializeCache() { if (!this.support.cacheAPI) { Logger.warn('[StorageManager] Cache API not supported'); return; } try { this.cache = await caches.open(this.config.cacheName || 'app-cache-v1'); Logger.info('[StorageManager] Cache API initialized'); } catch (error) { Logger.error('[StorageManager] Cache API initialization failed:', error); } } /** * IndexedDB Operations */ db = { // Set key-value data set: async (key, value, expiration = null) => { return new Promise((resolve, reject) => { if (!this.db) { reject(new Error('IndexedDB not available')); return; } const transaction = this.db.transaction(['keyValue'], 'readwrite'); const store = transaction.objectStore('keyValue'); const data = { key, value, timestamp: Date.now(), expiration: expiration ? Date.now() + expiration : null }; const request = store.put(data); request.onsuccess = () => { Logger.info(`[StorageManager] DB set: ${key}`); resolve(data); }; request.onerror = () => { Logger.error(`[StorageManager] DB set failed: ${key}`); reject(request.error); }; }); }, // Get key-value data get: async (key) => { return new Promise((resolve, reject) => { if (!this.db) { reject(new Error('IndexedDB not available')); return; } const transaction = this.db.transaction(['keyValue'], 'readonly'); const store = transaction.objectStore('keyValue'); const request = store.get(key); request.onsuccess = () => { const result = request.result; if (!result) { resolve(null); return; } // Check expiration if (result.expiration && Date.now() > result.expiration) { this.db.delete(key); // Auto-cleanup expired data resolve(null); return; } resolve(result.value); }; request.onerror = () => { Logger.error(`[StorageManager] DB get failed: ${key}`); reject(request.error); }; }); }, // Delete key delete: async (key) => { return new Promise((resolve, reject) => { if (!this.db) { reject(new Error('IndexedDB not available')); return; } const transaction = this.db.transaction(['keyValue'], 'readwrite'); const store = transaction.objectStore('keyValue'); const request = store.delete(key); request.onsuccess = () => { Logger.info(`[StorageManager] DB deleted: ${key}`); resolve(true); }; request.onerror = () => { reject(request.error); }; }); }, // Get all keys keys: async () => { return new Promise((resolve, reject) => { if (!this.db) { reject(new Error('IndexedDB not available')); return; } const transaction = this.db.transaction(['keyValue'], 'readonly'); const store = transaction.objectStore('keyValue'); const request = store.getAllKeys(); request.onsuccess = () => { resolve(request.result); }; request.onerror = () => { reject(request.error); }; }); }, // Clear all data clear: async () => { return new Promise((resolve, reject) => { if (!this.db) { reject(new Error('IndexedDB not available')); return; } const transaction = this.db.transaction(['keyValue'], 'readwrite'); const store = transaction.objectStore('keyValue'); const request = store.clear(); request.onsuccess = () => { Logger.info('[StorageManager] DB cleared'); resolve(true); }; request.onerror = () => { reject(request.error); }; }); }, // Store files/blobs storeFile: async (name, file, metadata = {}) => { return new Promise((resolve, reject) => { if (!this.db) { reject(new Error('IndexedDB not available')); return; } const transaction = this.db.transaction(['files'], 'readwrite'); const store = transaction.objectStore('files'); const fileData = { name, file, type: file.type, size: file.size, timestamp: Date.now(), metadata }; const request = store.add(fileData); request.onsuccess = () => { Logger.info(`[StorageManager] File stored: ${name}`); resolve({ id: request.result, ...fileData }); }; request.onerror = () => { reject(request.error); }; }); }, // Get file getFile: async (id) => { return new Promise((resolve, reject) => { if (!this.db) { reject(new Error('IndexedDB not available')); return; } const transaction = this.db.transaction(['files'], 'readonly'); const store = transaction.objectStore('files'); const request = store.get(id); request.onsuccess = () => { resolve(request.result); }; request.onerror = () => { reject(request.error); }; }); } }; /** * Cache API Operations */ cache = { // Add request/response to cache add: async (request, response = null) => { if (!this.cache) { throw new Error('Cache API not available'); } try { if (response) { await this.cache.put(request, response); } else { await this.cache.add(request); } Logger.info(`[StorageManager] Cache add: ${request}`); } catch (error) { Logger.error('[StorageManager] Cache add failed:', error); throw error; } }, // Get from cache get: async (request) => { if (!this.cache) { throw new Error('Cache API not available'); } try { const response = await this.cache.match(request); if (response) { Logger.info(`[StorageManager] Cache hit: ${request}`); } else { Logger.info(`[StorageManager] Cache miss: ${request}`); } return response; } catch (error) { Logger.error('[StorageManager] Cache get failed:', error); throw error; } }, // Delete from cache delete: async (request) => { if (!this.cache) { throw new Error('Cache API not available'); } try { const success = await this.cache.delete(request); if (success) { Logger.info(`[StorageManager] Cache deleted: ${request}`); } return success; } catch (error) { Logger.error('[StorageManager] Cache delete failed:', error); throw error; } }, // Get all cached requests keys: async () => { if (!this.cache) { throw new Error('Cache API not available'); } try { return await this.cache.keys(); } catch (error) { Logger.error('[StorageManager] Cache keys failed:', error); throw error; } }, // Clear cache clear: async () => { if (!this.cache) { throw new Error('Cache API not available'); } try { const keys = await this.cache.keys(); await Promise.all(keys.map(key => this.cache.delete(key))); Logger.info('[StorageManager] Cache cleared'); } catch (error) { Logger.error('[StorageManager] Cache clear failed:', error); throw error; } } }; /** * Broadcast Channel for cross-tab communication */ channel = { // Create or get channel create: (channelName, onMessage = null) => { if (!this.support.broadcastChannel) { Logger.warn('[StorageManager] BroadcastChannel not supported'); return null; } if (this.channels.has(channelName)) { return this.channels.get(channelName); } const channel = new BroadcastChannel(channelName); if (onMessage) { channel.addEventListener('message', onMessage); } const channelWrapper = { name: channelName, channel, send: (data) => { channel.postMessage({ data, timestamp: Date.now(), sender: 'current-tab' }); Logger.info(`[StorageManager] Broadcast sent to ${channelName}`); }, onMessage: (callback) => { channel.addEventListener('message', (event) => { callback(event.data); }); }, close: () => { channel.close(); this.channels.delete(channelName); Logger.info(`[StorageManager] Channel closed: ${channelName}`); } }; this.channels.set(channelName, channelWrapper); Logger.info(`[StorageManager] Channel created: ${channelName}`); return channelWrapper; }, // Get existing channel get: (channelName) => { return this.channels.get(channelName) || null; }, // Close channel close: (channelName) => { const channel = this.channels.get(channelName); if (channel) { channel.close(); } }, // Close all channels closeAll: () => { this.channels.forEach(channel => channel.close()); this.channels.clear(); Logger.info('[StorageManager] All channels closed'); } }; /** * Web Locks API for resource locking */ locks = { // Acquire lock acquire: async (lockName, callback, options = {}) => { if (!this.support.webLocks) { Logger.warn('[StorageManager] Web Locks not supported, executing without lock'); return await callback(); } try { return await navigator.locks.request(lockName, options, async (lock) => { Logger.info(`[StorageManager] Lock acquired: ${lockName}`); const result = await callback(lock); Logger.info(`[StorageManager] Lock released: ${lockName}`); return result; }); } catch (error) { Logger.error(`[StorageManager] Lock failed: ${lockName}`, error); throw error; } }, // Query locks query: async () => { if (!this.support.webLocks) { return { held: [], pending: [] }; } try { return await navigator.locks.query(); } catch (error) { Logger.error('[StorageManager] Lock query failed:', error); throw error; } } }; /** * Local/Session Storage helpers (with JSON support) */ local = { set: (key, value, expiration = null) => { if (!this.support.localStorage) return false; try { const data = { value, timestamp: Date.now(), expiration: expiration ? Date.now() + expiration : null }; localStorage.setItem(key, JSON.stringify(data)); return true; } catch (error) { Logger.error('[StorageManager] localStorage set failed:', error); return false; } }, get: (key) => { if (!this.support.localStorage) return null; try { const item = localStorage.getItem(key); if (!item) return null; const data = JSON.parse(item); // Check expiration if (data.expiration && Date.now() > data.expiration) { localStorage.removeItem(key); return null; } return data.value; } catch (error) { Logger.error('[StorageManager] localStorage get failed:', error); return null; } }, delete: (key) => { if (!this.support.localStorage) return false; localStorage.removeItem(key); return true; }, clear: () => { if (!this.support.localStorage) return false; localStorage.clear(); return true; }, keys: () => { if (!this.support.localStorage) return []; return Object.keys(localStorage); } }; session = { set: (key, value) => { if (!this.support.sessionStorage) return false; try { sessionStorage.setItem(key, JSON.stringify(value)); return true; } catch (error) { Logger.error('[StorageManager] sessionStorage set failed:', error); return false; } }, get: (key) => { if (!this.support.sessionStorage) return null; try { const item = sessionStorage.getItem(key); return item ? JSON.parse(item) : null; } catch (error) { Logger.error('[StorageManager] sessionStorage get failed:', error); return null; } }, delete: (key) => { if (!this.support.sessionStorage) return false; sessionStorage.removeItem(key); return true; }, clear: () => { if (!this.support.sessionStorage) return false; sessionStorage.clear(); return true; } }; /** * Smart caching with automatic expiration */ smartCache = { set: async (key, value, options = {}) => { const { storage = 'indexedDB', expiration = null, fallback = true } = options; try { if (storage === 'indexedDB' && this.db) { return await this.db.set(key, value, expiration); } else if (fallback) { return this.local.set(key, value, expiration); } } catch (error) { if (fallback) { return this.local.set(key, value, expiration); } throw error; } }, get: async (key, options = {}) => { const { storage = 'indexedDB', fallback = true } = options; try { if (storage === 'indexedDB' && this.db) { return await this.db.get(key); } else if (fallback) { return this.local.get(key); } } catch (error) { if (fallback) { return this.local.get(key); } throw error; } }, delete: async (key, options = {}) => { const { storage = 'indexedDB', fallback = true } = options; try { if (storage === 'indexedDB' && this.db) { await this.db.delete(key); } if (fallback) { this.local.delete(key); } } catch (error) { if (fallback) { this.local.delete(key); } throw error; } } }; /** * Get storage usage statistics */ async getStorageUsage() { const stats = { quota: 0, usage: 0, available: 0, percentage: 0 }; if ('storage' in navigator && 'estimate' in navigator.storage) { try { const estimate = await navigator.storage.estimate(); stats.quota = estimate.quota || 0; stats.usage = estimate.usage || 0; stats.available = stats.quota - stats.usage; stats.percentage = stats.quota > 0 ? Math.round((stats.usage / stats.quota) * 100) : 0; } catch (error) { Logger.error('[StorageManager] Storage estimate failed:', error); } } return stats; } /** * Cleanup expired data */ async cleanup() { Logger.info('[StorageManager] Starting cleanup...'); try { // Cleanup IndexedDB expired entries if (this.db) { const transaction = this.db.transaction(['keyValue'], 'readwrite'); const store = transaction.objectStore('keyValue'); const index = store.index('timestamp'); const request = index.openCursor(); let cleaned = 0; request.onsuccess = (event) => { const cursor = event.target.result; if (cursor) { const data = cursor.value; if (data.expiration && Date.now() > data.expiration) { cursor.delete(); cleaned++; } cursor.continue(); } else { Logger.info(`[StorageManager] Cleanup completed: ${cleaned} expired entries removed`); } }; } // Cleanup localStorage expired entries if (this.support.localStorage) { const keys = Object.keys(localStorage); let localCleaned = 0; keys.forEach(key => { try { const item = localStorage.getItem(key); const data = JSON.parse(item); if (data.expiration && Date.now() > data.expiration) { localStorage.removeItem(key); localCleaned++; } } catch (error) { // Ignore non-JSON items } }); if (localCleaned > 0) { Logger.info(`[StorageManager] LocalStorage cleanup: ${localCleaned} expired entries removed`); } } } catch (error) { Logger.error('[StorageManager] Cleanup failed:', error); } } /** * Get comprehensive storage status */ async getStatus() { const usage = await this.getStorageUsage(); return { support: this.support, usage, activeChannels: this.channels.size, dbConnected: !!this.db, cacheConnected: !!this.cache, channelNames: Array.from(this.channels.keys()) }; } }