// 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 = { dbName: config.dbName || 'AppDatabase', dbVersion: config.dbVersion || 1, enableAnalytics: config.enableAnalytics ?? true, enableQuotaMonitoring: config.enableQuotaMonitoring ?? true, quotaWarningThreshold: config.quotaWarningThreshold || 0.8, // 80% ...config }; this.db = null; this.cache = null; this.channels = new Map(); // Analytics this.analytics = { operations: { get: 0, set: 0, delete: 0, clear: 0 }, errors: 0, quotaWarnings: 0 }; // 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, storageEstimate: 'storage' in navigator && 'estimate' in navigator.storage }; Logger.info('[StorageManager] Initialized with support:', this.support); // Initialize databases this.initializeDB(); this.initializeCache(); // Start quota monitoring if enabled if (this.config.enableQuotaMonitoring) { this.startQuotaMonitoring(); } } /** * 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()), analytics: this.config.enableAnalytics ? this.getAnalytics() : null }; } /** * Unified storage API - works with any storage type */ storage = { /** * Set value in storage */ set: async (key, value, options = {}) => { const { type = 'auto', // 'auto' | 'localStorage' | 'sessionStorage' | 'indexedDB' expiration = null, ...rest } = options; try { // Auto-select storage type based on size let storageType = type; if (type === 'auto') { const valueSize = JSON.stringify(value).length; storageType = valueSize > 5 * 1024 * 1024 ? 'indexedDB' : 'localStorage'; // >5MB use IndexedDB } switch (storageType) { case 'localStorage': return this.local.set(key, value, expiration); case 'sessionStorage': return this.session.set(key, value); case 'indexedDB': return await this.db.set(key, value, expiration); default: throw new Error(`Unknown storage type: ${storageType}`); } } catch (error) { // Fallback to localStorage if IndexedDB fails if (type === 'indexedDB' || type === 'auto') { Logger.warn('[StorageManager] Falling back to localStorage', error); return this.local.set(key, value, expiration); } throw error; } finally { if (this.config.enableAnalytics) { this.analytics.operations.set++; } } }, /** * Get value from storage */ get: async (key, options = {}) => { const { type = 'auto', ...rest } = options; try { // Try different storage types in order if (type === 'auto') { // Try localStorage first (fastest) const localValue = this.local.get(key); if (localValue !== null) { if (this.config.enableAnalytics) { this.analytics.operations.get++; } return localValue; } // Try sessionStorage const sessionValue = this.session.get(key); if (sessionValue !== null) { if (this.config.enableAnalytics) { this.analytics.operations.get++; } return sessionValue; } // Try IndexedDB if (this.db) { const dbValue = await this.db.get(key); if (dbValue !== null) { if (this.config.enableAnalytics) { this.analytics.operations.get++; } return dbValue; } } return null; } else { switch (type) { case 'localStorage': const localValue = this.local.get(key); if (this.config.enableAnalytics) { this.analytics.operations.get++; } return localValue; case 'sessionStorage': const sessionValue = this.session.get(key); if (this.config.enableAnalytics) { this.analytics.operations.get++; } return sessionValue; case 'indexedDB': const dbValue = await this.db.get(key); if (this.config.enableAnalytics) { this.analytics.operations.get++; } return dbValue; default: throw new Error(`Unknown storage type: ${type}`); } } } catch (error) { Logger.error('[StorageManager] Storage get failed', error); if (this.config.enableAnalytics) { this.analytics.errors++; } throw error; } }, /** * Delete value from storage */ delete: async (key, options = {}) => { const { type = 'all', ...rest } = options; try { if (type === 'all') { // Delete from all storage types this.local.delete(key); this.session.delete(key); if (this.db) { await this.db.delete(key); } } else { switch (type) { case 'localStorage': this.local.delete(key); break; case 'sessionStorage': this.session.delete(key); break; case 'indexedDB': if (this.db) { await this.db.delete(key); } break; } } if (this.config.enableAnalytics) { this.analytics.operations.delete++; } } catch (error) { Logger.error('[StorageManager] Storage delete failed', error); if (this.config.enableAnalytics) { this.analytics.errors++; } throw error; } }, /** * Clear all storage */ clear: async (options = {}) => { const { type = 'all', ...rest } = options; try { if (type === 'all') { this.local.clear(); this.session.clear(); if (this.db) { await this.db.clear(); } if (this.cache) { await this.cache.clear(); } } else { switch (type) { case 'localStorage': this.local.clear(); break; case 'sessionStorage': this.session.clear(); break; case 'indexedDB': if (this.db) { await this.db.clear(); } break; case 'cache': if (this.cache) { await this.cache.clear(); } break; } } if (this.config.enableAnalytics) { this.analytics.operations.clear++; } } catch (error) { Logger.error('[StorageManager] Storage clear failed', error); if (this.config.enableAnalytics) { this.analytics.errors++; } throw error; } } }; /** * Start quota monitoring */ startQuotaMonitoring() { // Check quota periodically setInterval(async () => { try { const usage = await this.getStorageUsage(); if (usage.percentage >= this.config.quotaWarningThreshold * 100) { this.analytics.quotaWarnings++; Logger.warn('[StorageManager] Storage quota warning', { usage: usage.percentage + '%', available: this.formatBytes(usage.available) }); // Trigger event const event = new CustomEvent('storage:quota-warning', { detail: usage, bubbles: true }); window.dispatchEvent(event); } } catch (error) { Logger.error('[StorageManager] Quota monitoring error', error); } }, 60000); // Check every minute } /** * Migrate data between storage types */ async migrate(fromType, toType, keys = null) { Logger.info('[StorageManager] Starting migration', { fromType, toType }); let sourceKeys = keys; // Get keys from source if (!sourceKeys) { switch (fromType) { case 'localStorage': sourceKeys = this.local.keys(); break; case 'sessionStorage': sourceKeys = Object.keys(sessionStorage); break; case 'indexedDB': if (this.db) { sourceKeys = await this.db.keys(); } break; } } if (!sourceKeys || sourceKeys.length === 0) { Logger.info('[StorageManager] No keys to migrate'); return; } let migrated = 0; let failed = 0; for (const key of sourceKeys) { try { // Get value from source let value = null; switch (fromType) { case 'localStorage': value = this.local.get(key); break; case 'sessionStorage': value = this.session.get(key); break; case 'indexedDB': if (this.db) { value = await this.db.get(key); } break; } if (value === null) { continue; } // Set value in destination switch (toType) { case 'localStorage': this.local.set(key, value); break; case 'sessionStorage': this.session.set(key, value); break; case 'indexedDB': if (this.db) { await this.db.set(key, value); } break; } migrated++; } catch (error) { Logger.error(`[StorageManager] Failed to migrate key: ${key}`, error); failed++; } } Logger.info('[StorageManager] Migration completed', { migrated, failed }); return { migrated, failed }; } /** * Get storage analytics */ getAnalytics() { return { ...this.analytics, totalOperations: Object.values(this.analytics.operations).reduce((sum, count) => sum + count, 0), errorRate: this.analytics.errors / (this.analytics.operations.get + this.analytics.operations.set || 1) }; } /** * Reset analytics */ resetAnalytics() { this.analytics = { operations: { get: 0, set: 0, delete: 0, clear: 0 }, errors: 0, quotaWarnings: 0 }; } /** * Format bytes for human reading */ formatBytes(bytes) { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]; } }