/** * Storage utility that uses IndexedDB for table data and localStorage for config/secrets * * Strategy: * - IndexedDB: ALL table/datasource data (can store hundreds of MB) * - localStorage: Only for secrets, variables, configurations (small data) */ const DB_NAME = 'analitica-nucleo-db' const DB_VERSION = 1 const STORE_NAME = 'table-data' let dbInstance: IDBDatabase | null = null /** * Initialize IndexedDB */ async function initDB(): Promise { if (dbInstance) return dbInstance return new Promise((resolve, reject) => { const request = indexedDB.open(DB_NAME, DB_VERSION) request.onerror = () => { console.error('[Storage] IndexedDB error:', request.error) reject(request.error) } request.onsuccess = () => { dbInstance = request.result console.log('[Storage] IndexedDB initialized successfully') resolve(dbInstance) } request.onupgradeneeded = (event) => { const db = (event.target as IDBOpenDBRequest).result // Create object store if it doesn't exist if (!db.objectStoreNames.contains(STORE_NAME)) { db.createObjectStore(STORE_NAME, { keyPath: 'key' }) console.log('[Storage] Created object store:', STORE_NAME) } } }) } /** * Deep clone to remove reactivity and make data serializable for IndexedDB */ function toPlainObject(obj: any): any { // Use JSON parse/stringify to deep clone and remove all reactivity/proxies try { return JSON.parse(JSON.stringify(obj)) } catch (error) { console.error('[Storage] Failed to convert to plain object:', error) return obj } } /** * Save data to IndexedDB */ async function saveToIndexedDB(key: string, data: any): Promise { try { const db = await initDB() // Convert reactive objects to plain objects const plainData = toPlainObject(data) return new Promise((resolve, reject) => { const transaction = db.transaction([STORE_NAME], 'readwrite') const store = transaction.objectStore(STORE_NAME) const request = store.put({ key, value: plainData, timestamp: Date.now() }) request.onsuccess = () => { console.log(`[Storage] Saved to IndexedDB: ${key}`) resolve(true) } request.onerror = () => { console.error(`[Storage] Failed to save to IndexedDB: ${key}`, request.error) reject(request.error) } }) } catch (error) { console.error('[Storage] IndexedDB save error:', error) return false } } /** * Load data from IndexedDB */ async function loadFromIndexedDB(key: string): Promise { try { const db = await initDB() return new Promise((resolve, reject) => { const transaction = db.transaction([STORE_NAME], 'readonly') const store = transaction.objectStore(STORE_NAME) const request = store.get(key) request.onsuccess = () => { if (request.result) { console.log(`[Storage] Loaded from IndexedDB: ${key}`) resolve(request.result.value) } else { console.log(`[Storage] No data found in IndexedDB for: ${key}`) resolve(null) } } request.onerror = () => { console.error(`[Storage] Failed to load from IndexedDB: ${key}`, request.error) reject(request.error) } }) } catch (error) { console.error('[Storage] IndexedDB load error:', error) return null } } /** * Remove data from IndexedDB */ async function removeFromIndexedDB(key: string): Promise { try { const db = await initDB() return new Promise((resolve, reject) => { const transaction = db.transaction([STORE_NAME], 'readwrite') const store = transaction.objectStore(STORE_NAME) const request = store.delete(key) request.onsuccess = () => { console.log(`[Storage] Removed from IndexedDB: ${key}`) resolve(true) } request.onerror = () => { console.error(`[Storage] Failed to remove from IndexedDB: ${key}`, request.error) reject(request.error) } }) } catch (error) { console.error('[Storage] IndexedDB remove error:', error) return false } } /** * Storage interface for table/datasource data (uses IndexedDB exclusively) */ export const tableDataStorage = { /** * Save table data to IndexedDB (for large datasets) */ async setItem(key: string, data: any): Promise { if (!process.client) { console.log('[TableDataStorage] Not on client, skipping storage') return } const dataString = JSON.stringify(data) const dataSizeKB = new Blob([dataString]).size / 1024 console.log(`[TableDataStorage] Saving ${dataSizeKB.toFixed(2)} KB to IndexedDB for key: ${key}`) try { const success = await saveToIndexedDB(key, data) if (success) { console.log(`[TableDataStorage] ✓ Successfully saved ${dataSizeKB.toFixed(2)} KB to IndexedDB`) return } else { throw new Error('Failed to save to IndexedDB') } } catch (error) { console.error('[TableDataStorage] ✗ Failed to save to IndexedDB:', error) throw new Error(`No se pudo guardar la tabla: ${error}`) } }, /** * Load table data from IndexedDB */ async getItem(key: string): Promise { if (!process.client) { console.log('[TableDataStorage] Not on client, skipping storage') return null } console.log(`[TableDataStorage] Loading from IndexedDB: ${key}`) try { const data = await loadFromIndexedDB(key) if (data !== null) { const dataString = JSON.stringify(data) const dataSizeKB = new Blob([dataString]).size / 1024 console.log(`[TableDataStorage] ✓ Loaded ${dataSizeKB.toFixed(2)} KB from IndexedDB`) return data } else { console.log(`[TableDataStorage] No data found in IndexedDB for: ${key}`) return null } } catch (error) { console.error('[TableDataStorage] ✗ Failed to load from IndexedDB:', error) return null } }, /** * Remove table data from IndexedDB */ async removeItem(key: string): Promise { if (!process.client) return console.log(`[TableDataStorage] Removing from IndexedDB: ${key}`) try { await removeFromIndexedDB(key) console.log(`[TableDataStorage] ✓ Removed from IndexedDB: ${key}`) } catch (error) { console.error('[TableDataStorage] ✗ Failed to remove from IndexedDB:', error) } } } /** * Storage interface for config/secrets (uses localStorage exclusively) */ export const configStorage = { /** * Save config/secrets to localStorage (for small data only) */ setItem(key: string, data: any): void { if (!process.client) { console.log('[ConfigStorage] Not on client, skipping storage') return } const dataString = JSON.stringify(data) const dataSizeKB = new Blob([dataString]).size / 1024 console.log(`[ConfigStorage] Saving ${dataSizeKB.toFixed(2)} KB to localStorage for key: ${key}`) try { localStorage.setItem(key, dataString) console.log(`[ConfigStorage] ✓ Saved to localStorage`) } catch (error: any) { if (error.name === 'QuotaExceededError') { console.error('[ConfigStorage] ✗ localStorage quota exceeded') throw new Error('No hay espacio en localStorage para guardar la configuración') } throw error } }, /** * Load config/secrets from localStorage */ getItem(key: string): any | null { if (!process.client) { console.log('[ConfigStorage] Not on client, skipping storage') return null } console.log(`[ConfigStorage] Loading from localStorage: ${key}`) try { const item = localStorage.getItem(key) if (item) { const data = JSON.parse(item) const dataSizeKB = new Blob([item]).size / 1024 console.log(`[ConfigStorage] ✓ Loaded ${dataSizeKB.toFixed(2)} KB from localStorage`) return data } else { console.log(`[ConfigStorage] No data found in localStorage for: ${key}`) return null } } catch (error) { console.error('[ConfigStorage] ✗ Failed to load from localStorage:', error) return null } }, /** * Remove config/secrets from localStorage */ removeItem(key: string): void { if (!process.client) return console.log(`[ConfigStorage] Removing from localStorage: ${key}`) try { localStorage.removeItem(key) console.log(`[ConfigStorage] ✓ Removed from localStorage: ${key}`) } catch (error) { console.error('[ConfigStorage] ✗ Failed to remove from localStorage:', error) } } }